forked from MapComplete/MapComplete
Feature: faster theme search with indexing by lunr
This commit is contained in:
parent
7226c82009
commit
1723f268c0
6 changed files with 194 additions and 184 deletions
15
package-lock.json
generated
15
package-lock.json
generated
|
@ -47,6 +47,7 @@
|
||||||
"fake-dom": "^1.0.4",
|
"fake-dom": "^1.0.4",
|
||||||
"flowbite-svelte": "^0.47.4",
|
"flowbite-svelte": "^0.47.4",
|
||||||
"follow-redirects": "^1.15.9",
|
"follow-redirects": "^1.15.9",
|
||||||
|
"fuse.js": "^7.1.0",
|
||||||
"geojson2svg": "^2.0.2",
|
"geojson2svg": "^2.0.2",
|
||||||
"html-to-image": "^1.11.11",
|
"html-to-image": "^1.11.11",
|
||||||
"i18next-client": "^1.11.4",
|
"i18next-client": "^1.11.4",
|
||||||
|
@ -15781,6 +15782,15 @@
|
||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/fuse.js": {
|
||||||
|
"version": "7.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/fuse.js/-/fuse.js-7.1.0.tgz",
|
||||||
|
"integrity": "sha512-trLf4SzuuUxfusZADLINj+dE8clK1frKdmqiJNb1Es75fmI5oY6X2mxLVUciLLjxqw/xr72Dhy+lER6dGd02FQ==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/gensync": {
|
"node_modules/gensync": {
|
||||||
"version": "1.0.0-beta.2",
|
"version": "1.0.0-beta.2",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
@ -41193,6 +41203,11 @@
|
||||||
"resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz",
|
||||||
"integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ=="
|
"integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ=="
|
||||||
},
|
},
|
||||||
|
"fuse.js": {
|
||||||
|
"version": "7.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/fuse.js/-/fuse.js-7.1.0.tgz",
|
||||||
|
"integrity": "sha512-trLf4SzuuUxfusZADLINj+dE8clK1frKdmqiJNb1Es75fmI5oY6X2mxLVUciLLjxqw/xr72Dhy+lER6dGd02FQ=="
|
||||||
|
},
|
||||||
"gensync": {
|
"gensync": {
|
||||||
"version": "1.0.0-beta.2",
|
"version": "1.0.0-beta.2",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
|
|
@ -208,6 +208,7 @@
|
||||||
"fake-dom": "^1.0.4",
|
"fake-dom": "^1.0.4",
|
||||||
"flowbite-svelte": "^0.47.4",
|
"flowbite-svelte": "^0.47.4",
|
||||||
"follow-redirects": "^1.15.9",
|
"follow-redirects": "^1.15.9",
|
||||||
|
"fuse.js": "^7.1.0",
|
||||||
"geojson2svg": "^2.0.2",
|
"geojson2svg": "^2.0.2",
|
||||||
"html-to-image": "^1.11.11",
|
"html-to-image": "^1.11.11",
|
||||||
"i18next-client": "^1.11.4",
|
"i18next-client": "^1.11.4",
|
||||||
|
|
|
@ -9,16 +9,12 @@ import {
|
||||||
DoesImageExist,
|
DoesImageExist,
|
||||||
PrevalidateTheme,
|
PrevalidateTheme,
|
||||||
ValidateLayer,
|
ValidateLayer,
|
||||||
ValidateThemeEnsemble,
|
ValidateThemeEnsemble
|
||||||
} from "../src/Models/ThemeConfig/Conversion/Validation"
|
} from "../src/Models/ThemeConfig/Conversion/Validation"
|
||||||
import { Translation } from "../src/UI/i18n/Translation"
|
import { Translation } from "../src/UI/i18n/Translation"
|
||||||
import { PrepareLayer } from "../src/Models/ThemeConfig/Conversion/PrepareLayer"
|
import { PrepareLayer } from "../src/Models/ThemeConfig/Conversion/PrepareLayer"
|
||||||
import { PrepareTheme } from "../src/Models/ThemeConfig/Conversion/PrepareTheme"
|
import { PrepareTheme } from "../src/Models/ThemeConfig/Conversion/PrepareTheme"
|
||||||
import {
|
import { Conversion, DesugaringContext, DesugaringStep } from "../src/Models/ThemeConfig/Conversion/Conversion"
|
||||||
Conversion,
|
|
||||||
DesugaringContext,
|
|
||||||
DesugaringStep,
|
|
||||||
} from "../src/Models/ThemeConfig/Conversion/Conversion"
|
|
||||||
import { Utils } from "../src/Utils"
|
import { Utils } from "../src/Utils"
|
||||||
import Script from "./Script"
|
import Script from "./Script"
|
||||||
import { AllSharedLayers } from "../src/Customizations/AllSharedLayers"
|
import { AllSharedLayers } from "../src/Customizations/AllSharedLayers"
|
||||||
|
@ -267,6 +263,7 @@ class LayerOverviewUtils extends Script {
|
||||||
addWord(lang, tr[lang])
|
addWord(lang, tr[lang])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
addWord("*", l.id)
|
addWord("*", l.id)
|
||||||
addWords(l.title)
|
addWords(l.title)
|
||||||
addWords(l.description)
|
addWords(l.description)
|
||||||
|
@ -286,9 +283,9 @@ class LayerOverviewUtils extends Script {
|
||||||
| LayerConfigJson
|
| LayerConfigJson
|
||||||
| string
|
| string
|
||||||
| {
|
| {
|
||||||
builtin
|
builtin
|
||||||
}
|
}
|
||||||
)[]
|
)[]
|
||||||
}[],
|
}[],
|
||||||
sharedLayers: Map<string, LayerConfigJson>
|
sharedLayers: Map<string, LayerConfigJson>
|
||||||
) {
|
) {
|
||||||
|
@ -317,7 +314,10 @@ class LayerOverviewUtils extends Script {
|
||||||
hideFromOverview: theme.hideFromOverview,
|
hideFromOverview: theme.hideFromOverview,
|
||||||
mustHaveLanguage: theme.mustHaveLanguage,
|
mustHaveLanguage: theme.mustHaveLanguage,
|
||||||
keywords,
|
keywords,
|
||||||
layers: theme.layers.filter((l) => sharedLayers.has(l["id"])).map((l) => l["id"]),
|
layers: (<LayerConfigJson[]>theme.layers)
|
||||||
|
.filter((l) => sharedLayers.has(l.id))
|
||||||
|
.filter(l => l.minzoom < 17)
|
||||||
|
.map((l) => l.id)
|
||||||
}
|
}
|
||||||
perId.set(data.id, data)
|
perId.set(data.id, data)
|
||||||
}
|
}
|
||||||
|
@ -392,10 +392,10 @@ class LayerOverviewUtils extends Script {
|
||||||
tagRenderings: bootstrapTagRenderings,
|
tagRenderings: bootstrapTagRenderings,
|
||||||
tagRenderingOrder: bootstrapTagRenderingsOrder,
|
tagRenderingOrder: bootstrapTagRenderingsOrder,
|
||||||
sharedLayers: null,
|
sharedLayers: null,
|
||||||
publicLayers: null,
|
publicLayers: null
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
addTagRenderingsToContext: true,
|
addTagRenderingsToContext: true
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -431,7 +431,7 @@ class LayerOverviewUtils extends Script {
|
||||||
"src/assets/SocialImageBanner.svg",
|
"src/assets/SocialImageBanner.svg",
|
||||||
"src/assets/SocialImageRepo.svg",
|
"src/assets/SocialImageRepo.svg",
|
||||||
"src/assets/svg/osm-logo.svg",
|
"src/assets/svg/osm-logo.svg",
|
||||||
"src/assets/templates/*",
|
"src/assets/templates/*"
|
||||||
]
|
]
|
||||||
for (const path of allSvgs) {
|
for (const path of allSvgs) {
|
||||||
if (
|
if (
|
||||||
|
@ -456,8 +456,8 @@ class LayerOverviewUtils extends Script {
|
||||||
if (contents.indexOf("<text") > 0) {
|
if (contents.indexOf("<text") > 0) {
|
||||||
console.warn(
|
console.warn(
|
||||||
"The SVG at " +
|
"The SVG at " +
|
||||||
path +
|
path +
|
||||||
" contains a `text`-tag. This is highly discouraged. Every machine viewing your theme has their own font libary, and the font you choose might not be present, resulting in a different font being rendered. Solution: open your .svg in inkscape (or another program), select the text and convert it to a path"
|
" contains a `text`-tag. This is highly discouraged. Every machine viewing your theme has their own font libary, and the font you choose might not be present, resulting in a different font being rendered. Solution: open your .svg in inkscape (or another program), select the text and convert it to a path"
|
||||||
)
|
)
|
||||||
errCount++
|
errCount++
|
||||||
}
|
}
|
||||||
|
@ -529,7 +529,7 @@ class LayerOverviewUtils extends Script {
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
layers: Array.from(sharedLayers.values()).filter(
|
layers: Array.from(sharedLayers.values()).filter(
|
||||||
(l) => !(l["#no-index"] === "yes")
|
(l) => !(l["#no-index"] === "yes")
|
||||||
),
|
)
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -546,11 +546,11 @@ class LayerOverviewUtils extends Script {
|
||||||
// mapcomplete-changes shows an icon for each corresponding mapcomplete-theme
|
// mapcomplete-changes shows an icon for each corresponding mapcomplete-theme
|
||||||
const iconsPerTheme = Array.from(sharedThemes.values()).map((th) => ({
|
const iconsPerTheme = Array.from(sharedThemes.values()).map((th) => ({
|
||||||
if: "theme=" + th.id,
|
if: "theme=" + th.id,
|
||||||
then: th.icon,
|
then: th.icon
|
||||||
}))
|
}))
|
||||||
const proto: ThemeConfigJson = JSON.parse(
|
const proto: ThemeConfigJson = JSON.parse(
|
||||||
readFileSync("./assets/themes/mapcomplete-changes/mapcomplete-changes.proto.json", {
|
readFileSync("./assets/themes/mapcomplete-changes/mapcomplete-changes.proto.json", {
|
||||||
encoding: "utf8",
|
encoding: "utf8"
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
const protolayer = <LayerConfigJson>(
|
const protolayer = <LayerConfigJson>(
|
||||||
|
@ -566,7 +566,7 @@ class LayerOverviewUtils extends Script {
|
||||||
new DetectDuplicateFilters().convertStrict(
|
new DetectDuplicateFilters().convertStrict(
|
||||||
{
|
{
|
||||||
layers: ScriptUtils.getLayerFiles().map((f) => f.parsed),
|
layers: ScriptUtils.getLayerFiles().map((f) => f.parsed),
|
||||||
themes: ScriptUtils.getThemeFiles().map((f) => f.parsed),
|
themes: ScriptUtils.getThemeFiles().map((f) => f.parsed)
|
||||||
},
|
},
|
||||||
ConversionContext.construct([], [])
|
ConversionContext.construct([], [])
|
||||||
)
|
)
|
||||||
|
@ -614,7 +614,7 @@ class LayerOverviewUtils extends Script {
|
||||||
const state: DesugaringContext = {
|
const state: DesugaringContext = {
|
||||||
tagRenderings: LayerOverviewUtils.asDict(sharedTagRenderings),
|
tagRenderings: LayerOverviewUtils.asDict(sharedTagRenderings),
|
||||||
tagRenderingOrder: sharedTagRenderings.map((tr) => tr.id),
|
tagRenderingOrder: sharedTagRenderings.map((tr) => tr.id),
|
||||||
sharedLayers: AllSharedLayers.getSharedLayersConfigs(),
|
sharedLayers: AllSharedLayers.getSharedLayersConfigs()
|
||||||
}
|
}
|
||||||
const sharedLayers = new Map<string, LayerConfigJson>()
|
const sharedLayers = new Map<string, LayerConfigJson>()
|
||||||
const prepLayer = new PrepareLayer(state)
|
const prepLayer = new PrepareLayer(state)
|
||||||
|
@ -659,12 +659,12 @@ class LayerOverviewUtils extends Script {
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
"Recompiled layers " +
|
"Recompiled layers " +
|
||||||
recompiledLayers.join(", ") +
|
recompiledLayers.join(", ") +
|
||||||
" and skipped " +
|
" and skipped " +
|
||||||
skippedLayers.length +
|
skippedLayers.length +
|
||||||
" layers. Detected " +
|
" layers. Detected " +
|
||||||
warningCount +
|
warningCount +
|
||||||
" warnings"
|
" warnings"
|
||||||
)
|
)
|
||||||
// We always need the calculated tags of 'usersettings', so we export them separately
|
// We always need the calculated tags of 'usersettings', so we export them separately
|
||||||
this.extractJavascriptCodeForLayer(
|
this.extractJavascriptCodeForLayer(
|
||||||
|
@ -686,11 +686,11 @@ class LayerOverviewUtils extends Script {
|
||||||
private extractJavascriptCode(themeFile: ThemeConfigJson) {
|
private extractJavascriptCode(themeFile: ThemeConfigJson) {
|
||||||
const allCode = [
|
const allCode = [
|
||||||
"import {Feature} from 'geojson'",
|
"import {Feature} from 'geojson'",
|
||||||
'import { ExtraFuncType } from "../../../Logic/ExtraFunctions";',
|
"import { ExtraFuncType } from \"../../../Logic/ExtraFunctions\";",
|
||||||
'import { Utils } from "../../../Utils"',
|
"import { Utils } from \"../../../Utils\"",
|
||||||
"export class ThemeMetaTagging {",
|
"export class ThemeMetaTagging {",
|
||||||
" public static readonly themeName = " + JSON.stringify(themeFile.id),
|
" public static readonly themeName = " + JSON.stringify(themeFile.id),
|
||||||
"",
|
""
|
||||||
]
|
]
|
||||||
for (const layer of themeFile.layers) {
|
for (const layer of themeFile.layers) {
|
||||||
const l = <LayerConfigJson>layer
|
const l = <LayerConfigJson>layer
|
||||||
|
@ -699,8 +699,8 @@ class LayerOverviewUtils extends Script {
|
||||||
|
|
||||||
allCode.push(
|
allCode.push(
|
||||||
" public metaTaggging_for_" +
|
" public metaTaggging_for_" +
|
||||||
id +
|
id +
|
||||||
"(feat: Feature, helperFunctions: Record<ExtraFuncType, (feature: Feature) => Function>) {"
|
"(feat: Feature, helperFunctions: Record<ExtraFuncType, (feature: Feature) => Function>) {"
|
||||||
)
|
)
|
||||||
allCode.push(" const {" + ExtraFunctions.types.join(", ") + "} = helperFunctions")
|
allCode.push(" const {" + ExtraFunctions.types.join(", ") + "} = helperFunctions")
|
||||||
for (const line of code) {
|
for (const line of code) {
|
||||||
|
@ -711,10 +711,10 @@ class LayerOverviewUtils extends Script {
|
||||||
if (!isStrict) {
|
if (!isStrict) {
|
||||||
allCode.push(
|
allCode.push(
|
||||||
" Utils.AddLazyProperty(feat.properties, '" +
|
" Utils.AddLazyProperty(feat.properties, '" +
|
||||||
attributeName +
|
attributeName +
|
||||||
"', () => " +
|
"', () => " +
|
||||||
expression +
|
expression +
|
||||||
" ) "
|
" ) "
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
attributeName = attributeName.substring(0, attributeName.length - 1).trim()
|
attributeName = attributeName.substring(0, attributeName.length - 1).trim()
|
||||||
|
@ -754,7 +754,7 @@ class LayerOverviewUtils extends Script {
|
||||||
`/** This code is autogenerated - do not edit. Edit ./assets/layers/${l?.id}/${l?.id}.json instead */`,
|
`/** This code is autogenerated - do not edit. Edit ./assets/layers/${l?.id}/${l?.id}.json instead */`,
|
||||||
"export class ThemeMetaTagging {",
|
"export class ThemeMetaTagging {",
|
||||||
" public static readonly themeName = " + JSON.stringify(l.id),
|
" public static readonly themeName = " + JSON.stringify(l.id),
|
||||||
"",
|
""
|
||||||
]
|
]
|
||||||
const code = l.calculatedTags ?? []
|
const code = l.calculatedTags ?? []
|
||||||
|
|
||||||
|
@ -769,10 +769,10 @@ class LayerOverviewUtils extends Script {
|
||||||
if (!isStrict) {
|
if (!isStrict) {
|
||||||
allCode.push(
|
allCode.push(
|
||||||
" Utils.AddLazyProperty(feat.properties, '" +
|
" Utils.AddLazyProperty(feat.properties, '" +
|
||||||
attributeName +
|
attributeName +
|
||||||
"', () => " +
|
"', () => " +
|
||||||
expression +
|
expression +
|
||||||
" ) "
|
" ) "
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
attributeName = attributeName.substring(0, attributeName.length - 2).trim()
|
attributeName = attributeName.substring(0, attributeName.length - 2).trim()
|
||||||
|
@ -813,7 +813,7 @@ class LayerOverviewUtils extends Script {
|
||||||
sharedLayers,
|
sharedLayers,
|
||||||
tagRenderings: LayerOverviewUtils.asDict(trs),
|
tagRenderings: LayerOverviewUtils.asDict(trs),
|
||||||
tagRenderingOrder: trs.map((tr) => tr.id),
|
tagRenderingOrder: trs.map((tr) => tr.id),
|
||||||
publicLayers,
|
publicLayers
|
||||||
}
|
}
|
||||||
const knownTagRenderings = new Set<string>()
|
const knownTagRenderings = new Set<string>()
|
||||||
convertState.tagRenderings.forEach((_, key) => knownTagRenderings.add(key))
|
convertState.tagRenderings.forEach((_, key) => knownTagRenderings.add(key))
|
||||||
|
@ -870,7 +870,7 @@ class LayerOverviewUtils extends Script {
|
||||||
)
|
)
|
||||||
try {
|
try {
|
||||||
themeFile = new PrepareTheme(convertState, {
|
themeFile = new PrepareTheme(convertState, {
|
||||||
skipDefaultLayers: true,
|
skipDefaultLayers: true
|
||||||
}).convertStrict(
|
}).convertStrict(
|
||||||
themeFile,
|
themeFile,
|
||||||
ConversionContext.construct([themePath], ["PrepareLayer"])
|
ConversionContext.construct([themePath], ["PrepareLayer"])
|
||||||
|
@ -919,7 +919,7 @@ class LayerOverviewUtils extends Script {
|
||||||
const e: string = [
|
const e: string = [
|
||||||
`the icon for theme ${themeFile.id} is too small. Please rescale the icon at ${themeFile.icon}`,
|
`the icon for theme ${themeFile.id} is too small. Please rescale the icon at ${themeFile.icon}`,
|
||||||
`Even though an SVG is 'infinitely scaleable', the icon should be dimensioned bigger. One of the build steps of the theme does convert the image to a PNG (to serve as PWA-icon) and having a small dimension will cause blurry images.`,
|
`Even though an SVG is 'infinitely scaleable', the icon should be dimensioned bigger. One of the build steps of the theme does convert the image to a PNG (to serve as PWA-icon) and having a small dimension will cause blurry images.`,
|
||||||
` Width = ${width} height = ${height}; we recommend a size of at least 500px * 500px and to use a square aspect ratio.`,
|
` Width = ${width} height = ${height}; we recommend a size of at least 500px * 500px and to use a square aspect ratio.`
|
||||||
].join("\n")
|
].join("\n")
|
||||||
err(e)
|
err(e)
|
||||||
}
|
}
|
||||||
|
@ -956,7 +956,7 @@ class LayerOverviewUtils extends Script {
|
||||||
hideFromOverview: t.hideFromOverview ?? false,
|
hideFromOverview: t.hideFromOverview ?? false,
|
||||||
shortDescription:
|
shortDescription:
|
||||||
t.shortDescription ?? new Translation(t.description).FirstSentence(),
|
t.shortDescription ?? new Translation(t.description).FirstSentence(),
|
||||||
mustHaveLanguage: t.mustHaveLanguage?.length > 0,
|
mustHaveLanguage: t.mustHaveLanguage?.length > 0
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
sharedLayers
|
sharedLayers
|
||||||
|
@ -965,10 +965,10 @@ class LayerOverviewUtils extends Script {
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
"Recompiled themes " +
|
"Recompiled themes " +
|
||||||
recompiledThemes.join(", ") +
|
recompiledThemes.join(", ") +
|
||||||
" and skipped " +
|
" and skipped " +
|
||||||
skippedThemes.length +
|
skippedThemes.length +
|
||||||
" themes"
|
" themes"
|
||||||
)
|
)
|
||||||
|
|
||||||
return fixed
|
return fixed
|
||||||
|
|
|
@ -1,19 +1,109 @@
|
||||||
import ThemeConfig, { MinimalThemeInformation } from "../../Models/ThemeConfig/ThemeConfig"
|
import ThemeConfig, { MinimalThemeInformation } from "../../Models/ThemeConfig/ThemeConfig"
|
||||||
import { Store } from "../UIEventSource"
|
import { Store } from "../UIEventSource"
|
||||||
import UserRelatedState from "../State/UserRelatedState"
|
import UserRelatedState from "../State/UserRelatedState"
|
||||||
import { Utils } from "../../Utils"
|
|
||||||
import Locale from "../../UI/i18n/Locale"
|
|
||||||
import themeOverview from "../../assets/generated/theme_overview.json"
|
import themeOverview from "../../assets/generated/theme_overview.json"
|
||||||
import LayerSearch from "./LayerSearch"
|
|
||||||
import SearchUtils from "./SearchUtils"
|
|
||||||
import { OsmConnection } from "../Osm/OsmConnection"
|
import { OsmConnection } from "../Osm/OsmConnection"
|
||||||
import { AndroidPolyfill } from "../Web/AndroidPolyfill"
|
import { AndroidPolyfill } from "../Web/AndroidPolyfill"
|
||||||
|
import Fuse from "fuse.js"
|
||||||
|
import Constants from "../../Models/Constants"
|
||||||
|
import Locale from "../../UI/i18n/Locale"
|
||||||
|
import { Utils } from "../../Utils"
|
||||||
|
|
||||||
type ThemeSearchScore = {
|
|
||||||
theme: MinimalThemeInformation
|
export class ThemeSearchIndex {
|
||||||
lowest: number
|
|
||||||
perLayer?: Record<string, number>
|
private readonly themeIndex: Fuse<MinimalThemeInformation>
|
||||||
other: number
|
private readonly layerIndex: Fuse<{ id: string, description }>
|
||||||
|
|
||||||
|
constructor(language: string, themesToSearch?: MinimalThemeInformation[], layersToIgnore: string[] = []) {
|
||||||
|
const themes = themesToSearch ?? ThemeSearch.officialThemes?.themes
|
||||||
|
if (!themes) {
|
||||||
|
throw "No themes loaded. Did generate:layeroverview fail?"
|
||||||
|
}
|
||||||
|
const fuseOptions = {
|
||||||
|
ignoreLocation: true,
|
||||||
|
threshold: 0.2,
|
||||||
|
keys: [
|
||||||
|
{ name: "id", weight: 2 },
|
||||||
|
"title." + language,
|
||||||
|
"keywords." + language,
|
||||||
|
"shortDescription." + language
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
this.themeIndex = new Fuse(themes.filter(th => th.id !== "personal"), fuseOptions)
|
||||||
|
|
||||||
|
const toIgnore = new Set(layersToIgnore)
|
||||||
|
const layersAsList: { id: string, description: Record<string, string[]> }[] = []
|
||||||
|
for (const id in ThemeSearch.officialThemes.layers) {
|
||||||
|
if (Constants.isPriviliged(id)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (toIgnore.has(id)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const l: Record<string, string[]> = ThemeSearch.officialThemes.layers[id]
|
||||||
|
layersAsList.push({ id, description: l })
|
||||||
|
}
|
||||||
|
this.layerIndex = new Fuse(layersAsList, {
|
||||||
|
includeScore: true,
|
||||||
|
minMatchCharLength: 3,
|
||||||
|
ignoreLocation: true,
|
||||||
|
threshold: 0.02,
|
||||||
|
keys: ["id", "description." + language]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
public search(text: string, limit?: number): MinimalThemeInformation[] {
|
||||||
|
const scored = this.searchWithScores(text)
|
||||||
|
let result = Array.from(scored.entries())
|
||||||
|
result.sort((a, b) => b[0] - a[0])
|
||||||
|
if (limit) {
|
||||||
|
result = result.slice(0, limit)
|
||||||
|
}
|
||||||
|
return result.map(e => ThemeSearch.officialThemesById.get(e[0]))
|
||||||
|
}
|
||||||
|
|
||||||
|
public searchWithScores(text: string): Map<string, number> {
|
||||||
|
const result = new Map<string, number>()
|
||||||
|
const themeResults = this.themeIndex.search(text)
|
||||||
|
for (const themeResult of themeResults) {
|
||||||
|
result.set(themeResult.item.id, themeResult.score)
|
||||||
|
}
|
||||||
|
|
||||||
|
const layerResults = this.layerIndex.search(text)
|
||||||
|
|
||||||
|
for (const layer of layerResults) {
|
||||||
|
const matchingThemes = ThemeSearch.layersToThemes.get(layer.item.id)
|
||||||
|
const score = layer.score
|
||||||
|
matchingThemes?.forEach(th => {
|
||||||
|
const previous = result.get(th.id) ?? 10000
|
||||||
|
result.set(th.id, Math.min(previous, score * 5))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds a search index containing all public and visited themes, but ignoring the layers loaded by the current theme
|
||||||
|
*/
|
||||||
|
public static fromState(state: { osmConnection: OsmConnection; theme: ThemeConfig }): Store<ThemeSearchIndex> {
|
||||||
|
const layersToIgnore = state.theme.layers.filter((l) => l.isNormal()).map((l) => l.id)
|
||||||
|
const knownHidden: Store<string[]> = UserRelatedState.initDiscoveredHiddenThemes(
|
||||||
|
state.osmConnection
|
||||||
|
).map((list) => Utils.Dedup(list))
|
||||||
|
const otherThemes: MinimalThemeInformation[] = ThemeSearch.officialThemes.themes.filter(
|
||||||
|
(th) => th.id !== state.theme.id
|
||||||
|
)
|
||||||
|
return Locale.language.map(language => {
|
||||||
|
const themes = otherThemes.concat(...knownHidden.data.map(id => ThemeSearch.officialThemesById.get(id)))
|
||||||
|
return new ThemeSearchIndex(language, themes, layersToIgnore)
|
||||||
|
},
|
||||||
|
[knownHidden]
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class ThemeSearch {
|
export default class ThemeSearch {
|
||||||
|
@ -25,42 +115,26 @@ export default class ThemeSearch {
|
||||||
string,
|
string,
|
||||||
MinimalThemeInformation
|
MinimalThemeInformation
|
||||||
>()
|
>()
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* For every layer id, states which themes use the layer
|
||||||
|
*/
|
||||||
|
public static readonly layersToThemes: Map<string, MinimalThemeInformation[]> = new Map()
|
||||||
static {
|
static {
|
||||||
for (const th of ThemeSearch.officialThemes.themes ?? []) {
|
for (const th of ThemeSearch.officialThemes.themes ?? []) {
|
||||||
ThemeSearch.officialThemesById.set(th.id, th)
|
ThemeSearch.officialThemesById.set(th.id, th)
|
||||||
|
for (const layer of th.layers) {
|
||||||
|
let list = ThemeSearch.layersToThemes.get(layer)
|
||||||
|
if (!list) {
|
||||||
|
list = []
|
||||||
|
ThemeSearch.layersToThemes.set(layer, list)
|
||||||
|
}
|
||||||
|
list.push(th)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private readonly _knownHiddenThemes: Store<Set<string>>
|
|
||||||
private readonly _layersToIgnore: string[]
|
|
||||||
private readonly _otherThemes: MinimalThemeInformation[]
|
|
||||||
|
|
||||||
constructor(state: { osmConnection: OsmConnection; theme: ThemeConfig }) {
|
|
||||||
this._layersToIgnore = state.theme.layers.filter((l) => l.isNormal()).map((l) => l.id)
|
|
||||||
this._knownHiddenThemes = UserRelatedState.initDiscoveredHiddenThemes(
|
|
||||||
state.osmConnection
|
|
||||||
).map((list) => new Set(list))
|
|
||||||
this._otherThemes = ThemeSearch.officialThemes.themes.filter(
|
|
||||||
(th) => th.id !== state.theme.id
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
public search(query: string, limit: number, threshold: number = 3): MinimalThemeInformation[] {
|
|
||||||
if (query.length < 1) {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
const sorted = ThemeSearch.sortedByLowestScores(
|
|
||||||
query,
|
|
||||||
this._otherThemes,
|
|
||||||
this._layersToIgnore
|
|
||||||
)
|
|
||||||
return sorted
|
|
||||||
.filter((sorted) => sorted.lowest < threshold)
|
|
||||||
.map((th) => th.theme)
|
|
||||||
.filter((th) => !th.hideFromOverview || this._knownHiddenThemes.data.has(th.id))
|
|
||||||
.slice(0, limit)
|
|
||||||
}
|
|
||||||
|
|
||||||
public static createUrlFor(layout: { id: string }, state?: { layoutToUse?: { id } }): string {
|
public static createUrlFor(layout: { id: string }, state?: { layoutToUse?: { id } }): string {
|
||||||
if (layout === undefined) {
|
if (layout === undefined) {
|
||||||
return undefined
|
return undefined
|
||||||
|
@ -97,82 +171,5 @@ export default class ThemeSearch {
|
||||||
return `${linkPrefix}`
|
return `${linkPrefix}`
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a score based on textual search
|
|
||||||
*
|
|
||||||
* Note that, if `query.length < 3`, layers are _not_ searched because this takes too much time
|
|
||||||
* @param query
|
|
||||||
* @param themes
|
|
||||||
* @param ignoreLayers
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
private static scoreThemes(
|
|
||||||
query: string,
|
|
||||||
themes: MinimalThemeInformation[],
|
|
||||||
ignoreLayers: string[] = undefined
|
|
||||||
): Record<string, ThemeSearchScore> {
|
|
||||||
if (query?.length < 1) {
|
|
||||||
return undefined
|
|
||||||
}
|
|
||||||
themes = Utils.NoNullInplace(themes)
|
|
||||||
|
|
||||||
let options: { blacklist: Set<string> } = undefined
|
|
||||||
if (ignoreLayers?.length > 0) {
|
|
||||||
options = { blacklist: new Set(ignoreLayers) }
|
|
||||||
}
|
|
||||||
const layerScores = query.length < 3 ? {} : LayerSearch.scoreLayers(query, options)
|
|
||||||
const results: Record<string, ThemeSearchScore> = {}
|
|
||||||
for (const layoutInfo of themes) {
|
|
||||||
const theme = layoutInfo.id
|
|
||||||
if (theme === "personal") {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if (Utils.simplifyStringForSearch(theme) === query) {
|
|
||||||
results[theme] = {
|
|
||||||
theme: layoutInfo,
|
|
||||||
lowest: -1,
|
|
||||||
other: 0,
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
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(
|
|
||||||
SearchUtils.scoreKeywords(query, keywords),
|
|
||||||
SearchUtils.scoreKeywords(query, layoutInfo.keywords)
|
|
||||||
)
|
|
||||||
const lowest = Math.min(other, ...Object.values(perLayer))
|
|
||||||
results[theme] = {
|
|
||||||
theme: layoutInfo,
|
|
||||||
perLayer,
|
|
||||||
other,
|
|
||||||
lowest,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return results
|
|
||||||
}
|
|
||||||
|
|
||||||
public static sortedByLowestScores(
|
|
||||||
search: string,
|
|
||||||
themes: MinimalThemeInformation[],
|
|
||||||
ignoreLayers: string[] = []
|
|
||||||
): ThemeSearchScore[] {
|
|
||||||
const scored = Object.values(this.scoreThemes(search, themes, ignoreLayers))
|
|
||||||
scored.sort((a, b) => a.lowest - b.lowest)
|
|
||||||
return scored
|
|
||||||
}
|
|
||||||
|
|
||||||
public static sortedByLowest(
|
|
||||||
search: string,
|
|
||||||
themes: MinimalThemeInformation[],
|
|
||||||
ignoreLayers: string[] = []
|
|
||||||
): MinimalThemeInformation[] {
|
|
||||||
return this.sortedByLowestScores(search, themes, ignoreLayers).map((th) => th.theme)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,7 @@ import CombinedSearcher from "../Search/CombinedSearcher"
|
||||||
import FilterSearch, { FilterSearchResult } from "../Search/FilterSearch"
|
import FilterSearch, { FilterSearchResult } from "../Search/FilterSearch"
|
||||||
import LocalElementSearch from "../Search/LocalElementSearch"
|
import LocalElementSearch from "../Search/LocalElementSearch"
|
||||||
import CoordinateSearch from "../Search/CoordinateSearch"
|
import CoordinateSearch from "../Search/CoordinateSearch"
|
||||||
import ThemeSearch from "../Search/ThemeSearch"
|
import { ThemeSearchIndex } from "../Search/ThemeSearch"
|
||||||
import OpenStreetMapIdSearch from "../Search/OpenStreetMapIdSearch"
|
import OpenStreetMapIdSearch from "../Search/OpenStreetMapIdSearch"
|
||||||
import PhotonSearch from "../Search/PhotonSearch"
|
import PhotonSearch from "../Search/PhotonSearch"
|
||||||
import ThemeViewState from "../../Models/ThemeViewState"
|
import ThemeViewState from "../../Models/ThemeViewState"
|
||||||
|
@ -67,8 +67,8 @@ export default class SearchState {
|
||||||
Stores.concat(suggestions).map((suggestions) => CombinedSearcher.merge(suggestions))
|
Stores.concat(suggestions).map((suggestions) => CombinedSearcher.merge(suggestions))
|
||||||
)
|
)
|
||||||
|
|
||||||
const themeSearch = new ThemeSearch(state)
|
const themeSearch = ThemeSearchIndex.fromState(state)
|
||||||
this.themeSuggestions = this.searchTerm.mapD((query) => themeSearch.search(query, 3))
|
this.themeSuggestions = this.searchTerm.mapD((query) => themeSearch.data.search(query, 3), [themeSearch])
|
||||||
|
|
||||||
const layerSearch = new LayerSearch(state.theme)
|
const layerSearch = new LayerSearch(state.theme)
|
||||||
this.layerSuggestions = this.searchTerm.mapD((query) => layerSearch.search(query, 5))
|
this.layerSuggestions = this.searchTerm.mapD((query) => layerSearch.search(query, 5))
|
||||||
|
|
|
@ -18,15 +18,15 @@
|
||||||
import Mastodon from "../assets/svg/Mastodon.svelte"
|
import Mastodon from "../assets/svg/Mastodon.svelte"
|
||||||
import Liberapay from "../assets/svg/Liberapay.svelte"
|
import Liberapay from "../assets/svg/Liberapay.svelte"
|
||||||
import Bug from "../assets/svg/Bug.svelte"
|
import Bug from "../assets/svg/Bug.svelte"
|
||||||
import Github from "../assets/svg/Github.svelte"
|
|
||||||
import { Utils } from "../Utils"
|
import { Utils } from "../Utils"
|
||||||
import { ArrowTrendingUp } from "@babeard/svelte-heroicons/solid/ArrowTrendingUp"
|
import { ArrowTrendingUp } from "@babeard/svelte-heroicons/solid/ArrowTrendingUp"
|
||||||
import Searchbar from "./Base/Searchbar.svelte"
|
import Searchbar from "./Base/Searchbar.svelte"
|
||||||
import ThemeSearch from "../Logic/Search/ThemeSearch"
|
import ThemeSearch, { ThemeSearchIndex } from "../Logic/Search/ThemeSearch"
|
||||||
import SearchUtils from "../Logic/Search/SearchUtils"
|
import SearchUtils from "../Logic/Search/SearchUtils"
|
||||||
import ChevronDoubleRight from "@babeard/svelte-heroicons/mini/ChevronDoubleRight"
|
import ChevronDoubleRight from "@babeard/svelte-heroicons/mini/ChevronDoubleRight"
|
||||||
import { AndroidPolyfill } from "../Logic/Web/AndroidPolyfill"
|
import { AndroidPolyfill } from "../Logic/Web/AndroidPolyfill"
|
||||||
import Forgejo from "../assets/svg/Forgejo.svelte"
|
import Forgejo from "../assets/svg/Forgejo.svelte"
|
||||||
|
import Locale from "./i18n/Locale"
|
||||||
AndroidPolyfill.init().then(() => console.log("Android polyfill setup completed"))
|
AndroidPolyfill.init().then(() => console.log("Android polyfill setup completed"))
|
||||||
const featureSwitches = new OsmConnectionFeatureSwitches()
|
const featureSwitches = new OsmConnectionFeatureSwitches()
|
||||||
const osmConnection = new OsmConnection({
|
const osmConnection = new OsmConnection({
|
||||||
|
@ -65,29 +65,26 @@
|
||||||
state.installedUserThemes
|
state.installedUserThemes
|
||||||
).mapD((stableIds) => Utils.NoNullInplace(stableIds.map((id) => state.getUnofficialTheme(id))))
|
).mapD((stableIds) => Utils.NoNullInplace(stableIds.map((id) => state.getUnofficialTheme(id))))
|
||||||
function filtered(themes: Store<MinimalThemeInformation[]>): Store<MinimalThemeInformation[]> {
|
function filtered(themes: Store<MinimalThemeInformation[]>): Store<MinimalThemeInformation[]> {
|
||||||
|
const searchIndex = Locale.language.map(language => {
|
||||||
|
return new ThemeSearchIndex(language, themes.data)
|
||||||
|
}, [themes])
|
||||||
|
|
||||||
|
|
||||||
return searchStable.map(
|
return searchStable.map(
|
||||||
(search) => {
|
(searchTerm) => {
|
||||||
if (!themes.data) {
|
if (!themes.data) {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
if (!search) {
|
if (!searchTerm) {
|
||||||
return themes.data
|
return themes.data
|
||||||
}
|
}
|
||||||
|
|
||||||
const start = new Date().getTime()
|
const index = searchIndex.data
|
||||||
const scores = ThemeSearch.sortedByLowestScores(search, themes.data)
|
|
||||||
const end = new Date().getTime()
|
return index.search(searchTerm)
|
||||||
console.trace("Scores for", search, "are", scores, "searching took", end - start, "ms")
|
|
||||||
const strict = scores.filter((sc) => sc.lowest < 2)
|
|
||||||
if (strict.length > 0) {
|
|
||||||
return strict.map((sc) => sc.theme)
|
|
||||||
}
|
|
||||||
return scores
|
|
||||||
.filter((sc) => sc.lowest < 4)
|
|
||||||
.slice(0, 6)
|
|
||||||
.map((sc) => sc.theme)
|
|
||||||
},
|
},
|
||||||
[themes]
|
[searchIndex]
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue