diff --git a/assets/themes/mapcomplete-changes/mapcomplete-changes.json b/assets/themes/mapcomplete-changes/mapcomplete-changes.json index ac8e738f6..a20472d7a 100644 --- a/assets/themes/mapcomplete-changes/mapcomplete-changes.json +++ b/assets/themes/mapcomplete-changes/mapcomplete-changes.json @@ -4,14 +4,14 @@ "en": "Changes made with MapComplete", "de": "Änderungen mit MapComplete vorgenommen" }, - "description": { - "en": "This maps shows all the changes made with MapComplete", - "de": "Diese Karte zeigt alle mit MapComplete vorgenommenen Änderungen" - }, "shortDescription": { "en": "Shows changes made by MapComplete", "de": "Änderungen von MapComplete anzeigen" }, + "description": { + "en": "This maps shows all the changes made with MapComplete", + "de": "Diese Karte zeigt alle mit MapComplete vorgenommenen Änderungen" + }, "icon": "./assets/svg/logo.svg", "hideFromOverview": true, "startLat": 0, @@ -730,4 +730,4 @@ } } ] -} +} \ No newline at end of file diff --git a/public/css/index-tailwind-output.css b/public/css/index-tailwind-output.css index 360ad4a65..0d9189e9f 100644 --- a/public/css/index-tailwind-output.css +++ b/public/css/index-tailwind-output.css @@ -1413,11 +1413,6 @@ input[type="range"].range-lg::-moz-range-thumb { margin-right: auto; } -.mx-3 { - margin-left: 0.75rem; - margin-right: 0.75rem; -} - .my-4 { margin-top: 1rem; margin-bottom: 1rem; @@ -1511,8 +1506,8 @@ input[type="range"].range-lg::-moz-range-thumb { margin-left: 0.5rem; } -.ml-4 { - margin-left: 1rem; +.mr-3 { + margin-right: 0.75rem; } .mb-2 { @@ -1531,6 +1526,10 @@ input[type="range"].range-lg::-moz-range-thumb { margin-bottom: 0.25rem; } +.ml-4 { + margin-left: 1rem; +} + .mt-8 { margin-top: 2rem; } @@ -3913,6 +3912,11 @@ input[type="range"].range-lg::-moz-range-thumb { padding-bottom: 0.25rem; } +.px-0 { + padding-left: 0px; + padding-right: 0px; +} + .px-3 { padding-left: 0.75rem; padding-right: 0.75rem; @@ -3978,11 +3982,6 @@ input[type="range"].range-lg::-moz-range-thumb { padding-bottom: 0.875rem; } -.px-0 { - padding-left: 0px; - padding-right: 0px; -} - .\!px-0 { padding-left: 0px !important; padding-right: 0px !important; @@ -4005,10 +4004,6 @@ input[type="range"].range-lg::-moz-range-thumb { padding-left: 0.5rem; } -.pl-1 { - padding-left: 0.25rem; -} - .pr-1 { padding-right: 0.25rem; } @@ -4037,6 +4032,10 @@ input[type="range"].range-lg::-moz-range-thumb { padding-right: 1rem; } +.pl-1 { + padding-left: 0.25rem; +} + .pb-1\.5 { padding-bottom: 0.375rem; } @@ -4664,6 +4663,10 @@ input[type="range"].range-lg::-moz-range-thumb { color: rgb(200 30 30 / var(--tw-placeholder-opacity)); } +.accent-gray-600 { + accent-color: #4B5563; +} + .opacity-50 { opacity: 0.5; } @@ -8113,10 +8116,6 @@ svg.apply-fill path { margin-right: 0.25rem; } - .sm\:mt-0 { - margin-top: 0px; - } - .sm\:mt-2 { margin-top: 0.5rem; } diff --git a/scripts/generateLayerOverview.ts b/scripts/generateLayerOverview.ts index 7b52805e4..18e98ed02 100644 --- a/scripts/generateLayerOverview.ts +++ b/scripts/generateLayerOverview.ts @@ -29,13 +29,12 @@ import LayerConfig from "../src/Models/ThemeConfig/LayerConfig" import PointRenderingConfig from "../src/Models/ThemeConfig/PointRenderingConfig" import { ConversionContext } from "../src/Models/ThemeConfig/Conversion/ConversionContext" import { GenerateFavouritesLayer } from "./generateFavouritesLayer" -import LayoutConfig from "../src/Models/ThemeConfig/LayoutConfig" +import LayoutConfig, { MinimalLayoutInformation } from "../src/Models/ThemeConfig/LayoutConfig" import Translations from "../src/UI/i18n/Translations" import { Translatable } from "../src/Models/ThemeConfig/Json/Translatable" import { ValidateThemeAndLayers } from "../src/Models/ThemeConfig/Conversion/ValidateThemeAndLayers" import { ExtractImages } from "../src/Models/ThemeConfig/Conversion/FixImages" import { - MinimalTagRenderingConfigJson, TagRenderingConfigJson, } from "../src/Models/ThemeConfig/Json/TagRenderingConfigJson" @@ -189,7 +188,7 @@ class LayerOverviewUtils extends Script { return publicLayerIds } - public static cleanTranslation(t: Record | Translation): Translatable { + public static cleanTranslation(t: string | Record | Translation): Translatable { return Translations.T(t).OnEveryLanguage((s) => parse_html(s).textContent).translations } @@ -212,11 +211,71 @@ class LayerOverviewUtils extends Script { return false } + static mergeKeywords(into: Record, source: Readonly>){ + for (const key in source) { + if(into[key]){ + into[key].push(...source[key]) + }else{ + into[key] = source[key] + } + } + } + + private layerKeywords(l: LayerConfigJson): Record { + const keywords: Record = {} + + function addWord(language: string, word: string | string[]) { + if(Array.isArray(word)){ + word.forEach(w => addWord(language, w)) + return + } + + word = Utils.SubstituteKeys(word, {}).trim() + if(!word){ + return + } + if (!keywords[language]) { + keywords[language] = [] + } + keywords[language].push(word) + } + + function addWords(tr: string | Record | Record | TagRenderingConfigJson) { + if(!tr){ + return + } + if (typeof tr === "string") { + addWord("*", tr) + return + } + if (tr["render"] !== undefined || tr["mappings"] !== undefined) { + tr = tr + addWords(tr.render) + for (const mapping of tr.mappings ?? []) { + if (typeof mapping === "string") { + addWords(mapping) + continue + } + addWords(mapping.then) + } + return + } + for (const lang in tr) { + addWord(lang, tr[lang]) + } + } + addWord("*", l.id) + addWords(l.title) + addWords(l.description) + addWords(l.searchTerms) + return keywords + } + writeSmallOverview( themes: { id: string - title: any - shortDescription: any + title: Translatable + shortDescription: Translatable icon: string hideFromOverview: boolean mustHaveLanguage: boolean @@ -228,62 +287,31 @@ class LayerOverviewUtils extends Script { } )[] }[], + sharedLayers: Map ) { - const perId = new Map() + const layerKeywords : Record> = {} + + sharedLayers.forEach((layer, id) => { + layerKeywords[id] = this.layerKeywords(layer) + }) + + const perId = new Map() for (const theme of themes) { + const keywords: Record = {} - - function addWord(language: string, word: string | string[]) { - if(Array.isArray(word)){ - word.forEach(w => addWord(language, w)) - return - } - - word = Utils.SubstituteKeys(word, {}).trim() - if(!word){ - return - } - console.log(language, "--->", word) - if (!keywords[language]) { - keywords[language] = [] - } - keywords[language].push(word) - } - - function addWords(tr: string | Record | Record | TagRenderingConfigJson) { - if(!tr){ - return - } - if (typeof tr === "string") { - addWord("*", tr) - return - } - if (tr["render"] !== undefined || tr["mappings"] !== undefined) { - tr = tr - addWords(tr.render) - for (let mapping of tr.mappings ?? []) { - if (typeof mapping === "string") { - addWords(mapping) - continue - } - addWords(mapping.then) - } - return - } - for (const lang in tr) { - addWord(lang, tr[lang]) - } - } - for (const layer of theme.layers ?? []) { const l = layer - addWord("*", l.id) - addWords(l.title) - addWords(l.description) - addWords(l.searchTerms) + if(sharedLayers.has(l.id)){ + continue + } + if(l.id.startsWith("note_import")){ + continue + } + LayerOverviewUtils.mergeKeywords(keywords, this.layerKeywords(l)) + } - const data = { + const data = { id: theme.id, title: theme.title, shortDescription: LayerOverviewUtils.cleanTranslation(theme.shortDescription), @@ -291,6 +319,7 @@ class LayerOverviewUtils extends Script { hideFromOverview: theme.hideFromOverview, mustHaveLanguage: theme.mustHaveLanguage, keywords, + layers: theme.layers.filter(l => sharedLayers.has(l["id"])).map(l => l["id"]) } perId.set(theme.id, data) } @@ -311,7 +340,7 @@ class LayerOverviewUtils extends Script { writeFileSync( "./src/assets/generated/theme_overview.json", - JSON.stringify(sorted, null, " "), + JSON.stringify({ layers: layerKeywords, themes: sorted }, null, " "), { encoding: "utf8" }, ) } @@ -927,7 +956,7 @@ class LayerOverviewUtils extends Script { if (whitelist.size == 0) { this.writeSmallOverview( Array.from(fixed.values()).map((t) => { - return { + return { ...t, hideFromOverview: t.hideFromOverview ?? false, shortDescription: @@ -935,6 +964,7 @@ class LayerOverviewUtils extends Script { mustHaveLanguage: t.mustHaveLanguage?.length > 0, } }), + sharedLayers ) } diff --git a/src/Logic/Geocoding/FilterSearch.ts b/src/Logic/Geocoding/FilterSearch.ts index 35b312017..1324f7935 100644 --- a/src/Logic/Geocoding/FilterSearch.ts +++ b/src/Logic/Geocoding/FilterSearch.ts @@ -7,11 +7,9 @@ import Constants from "../../Models/Constants" export default class FilterSearch implements GeocodingProvider { private readonly _state: SpecialVisualizationState - private readonly suggestions constructor(state: SpecialVisualizationState) { this._state = state - this.suggestions = this.getSuggestions() } async search(query: string): Promise { @@ -58,7 +56,6 @@ export default class FilterSearch implements GeocodingProvider { Utils.NoNullInplace(terms) const distances = queries.flatMap(query => terms.map(entry => { const d = Utils.levenshteinDistance(query, entry.slice(0, query.length)) - console.log(query, "? +", terms, "=", d) const dRelative = d / query.length return dRelative })) @@ -79,10 +76,10 @@ export default class FilterSearch implements GeocodingProvider { } + /** + * Create a random list of filters + */ getSuggestions(): FilterPayload[] { - if (this.suggestions) { - // return this.suggestions - } const result: FilterPayload[] = [] for (const [id, filteredLayer] of this._state.layerState.filteredLayers) { if (!Array.isArray(filteredLayer.layerDef.filters)) { diff --git a/src/Logic/Geocoding/RecentSearch.ts b/src/Logic/Geocoding/RecentSearch.ts index 6abf99749..5af34fa2a 100644 --- a/src/Logic/Geocoding/RecentSearch.ts +++ b/src/Logic/Geocoding/RecentSearch.ts @@ -13,7 +13,6 @@ export class RecentSearch { constructor(state: { layout: LayoutConfig, osmConnection: OsmConnection, selectedElement: Store }) { const prefs = state.osmConnection.preferencesHandler.GetLongPreference("previous-searches") - prefs.set(null) this._seenThisSession = new UIEventSource([])//UIEventSource.asObject(prefs, []) this.seenThisSession = this._seenThisSession diff --git a/src/Logic/Geocoding/ThemeSearch.ts b/src/Logic/Geocoding/ThemeSearch.ts index 06d30d351..254c8ba82 100644 --- a/src/Logic/Geocoding/ThemeSearch.ts +++ b/src/Logic/Geocoding/ThemeSearch.ts @@ -1,8 +1,7 @@ -import GeocodingProvider, { SearchResult, GeocodingOptions } from "./GeocodingProvider" +import GeocodingProvider, { GeocodingOptions, SearchResult } 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 { ImmutableStore, Store } from "../UIEventSource" @@ -12,11 +11,16 @@ export default class ThemeSearch implements GeocodingProvider { private readonly _state: SpecialVisualizationState private readonly _knownHiddenThemes: Store> private readonly _suggestionLimit: number + private readonly _layersToIgnore: string[] + private readonly _otherThemes: MinimalLayoutInformation[] constructor(state: SpecialVisualizationState, suggestionLimit: number) { this._state = state + this._layersToIgnore = state.layout.layers.map(l => l.id) this._suggestionLimit = suggestionLimit this._knownHiddenThemes = MoreScreen.knownHiddenThemes(this._state.osmConnection) + this._otherThemes = MoreScreen.officialThemes.themes + .filter(th => th.id !== state.layout.id) } async search(query: string): Promise { @@ -40,11 +44,11 @@ export default class ThemeSearch implements GeocodingProvider { if (query.length < 1) { return [] } - query = Utils.simplifyStringForSearch(query) - return ThemeSearch.allThemes + const sorted = MoreScreen.sortedByLowest(query, this._otherThemes, this._layersToIgnore) + console.log(">>>", sorted) + return sorted + .map(th => th.theme) .filter(th => !th.hideFromOverview || this._knownHiddenThemes.data.has(th.id)) - .filter(th => th.id !== this._state.layout.id) - .filter(th => MoreScreen.MatchesLayout(th, query)) .slice(0, limit) } diff --git a/src/Logic/State/SearchState.ts b/src/Logic/State/SearchState.ts index cac9ee5c1..57e0c7ebe 100644 --- a/src/Logic/State/SearchState.ts +++ b/src/Logic/State/SearchState.ts @@ -154,7 +154,7 @@ export default class SearchState { const poi = result[0] if (poi.category === "theme") { const theme = poi.payload - const url = MoreScreen.createUrlFor(theme, false) + const url = MoreScreen.createUrlFor(theme) window.location = url return true } diff --git a/src/Logic/State/UserRelatedState.ts b/src/Logic/State/UserRelatedState.ts index ad35a17fe..a6afd4fb7 100644 --- a/src/Logic/State/UserRelatedState.ts +++ b/src/Logic/State/UserRelatedState.ts @@ -1,4 +1,4 @@ -import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig" +import LayoutConfig, { MinimalLayoutInformation } from "../../Models/ThemeConfig/LayoutConfig" import { OsmConnection } from "../Osm/OsmConnection" import { MangroveIdentity } from "../Web/MangroveReviews" import { Store, Stores, UIEventSource } from "../UIEventSource" @@ -141,8 +141,9 @@ export default class UserRelatedState { this._recentlyVisitedThemes = UIEventSource.asObject(prefs.GetLongPreference("recently-visited-themes"), []) this.recentlyVisitedThemes = this._recentlyVisitedThemes if (layout) { - const osmConn =this.osmConnection + const osmConn = this.osmConnection const recentlyVisited = this._recentlyVisitedThemes + function update() { if (!osmConn.isLoggedIn.data) { return @@ -203,16 +204,7 @@ export default class UserRelatedState { } } - public GetUnofficialTheme(id: string): - | { - id: string - icon: string - title: any - shortDescription: any - definition?: any - isOfficial: boolean - } - | undefined { + public getUnofficialTheme(id: string): (MinimalLayoutInformation & { definition }) | undefined { const pref = this.osmConnection.GetLongPreference("unofficial-theme-" + id) const str = pref.data @@ -222,16 +214,7 @@ export default class UserRelatedState { } try { - const value: { - id: string - icon: string - title: any - shortDescription: any - definition?: any - isOfficial: boolean - } = JSON.parse(str) - value.isOfficial = false - return value + return JSON.parse(str) } catch (e) { console.warn( "Removing theme " + @@ -464,7 +447,7 @@ export default class UserRelatedState { } if (tags[key + "-combined-0"]) { // A combined value exists - if(tags[key].startsWith("undefined")){ + if (tags[key].startsWith("undefined")) { // Sometimes, a long string of 'undefined' will show up, we ignore them continue } diff --git a/src/Models/ThemeConfig/LayoutConfig.ts b/src/Models/ThemeConfig/LayoutConfig.ts index acc4c958b..2fb04ba13 100644 --- a/src/Models/ThemeConfig/LayoutConfig.ts +++ b/src/Models/ThemeConfig/LayoutConfig.ts @@ -22,10 +22,10 @@ export class MinimalLayoutInformation { icon: string title: Translatable shortDescription: Translatable - definition?: Translatable mustHaveLanguage?: boolean hideFromOverview?: boolean keywords?: Record + layers: string[] } /** * Minimal information about a theme diff --git a/src/UI/AllThemesGui.svelte b/src/UI/AllThemesGui.svelte index 2e62f868c..0bdbdca4b 100644 --- a/src/UI/AllThemesGui.svelte +++ b/src/UI/AllThemesGui.svelte @@ -11,16 +11,11 @@ import LoginToggle from "./Base/LoginToggle.svelte" import Pencil from "../assets/svg/Pencil.svelte" import Constants from "../Models/Constants" - import { Store, UIEventSource } from "../Logic/UIEventSource" - import { placeholder } from "../Utils/placeholder" - import { SearchIcon } from "@rgossiaux/svelte-heroicons/solid" + import { Store, Stores, UIEventSource } from "../Logic/UIEventSource" import ThemesList from "./BigComponents/ThemesList.svelte" - import { LayoutInformation } from "../Models/ThemeConfig/LayoutConfig" - import * as themeOverview from "../assets/generated/theme_overview.json" - import UnofficialThemeList from "./BigComponents/UnofficialThemeList.svelte" + import { MinimalLayoutInformation } from "../Models/ThemeConfig/LayoutConfig" import Eye from "../assets/svg/Eye.svelte" import LoginButton from "./Base/LoginButton.svelte" - import ChevronDoubleRight from "@babeard/svelte-heroicons/mini/ChevronDoubleRight" import Mastodon from "../assets/svg/Mastodon.svelte" import Liberapay from "../assets/svg/Liberapay.svelte" import Bug from "../assets/svg/Bug.svelte" @@ -28,6 +23,7 @@ import { Utils } from "../Utils" import { ArrowTrendingUp } from "@babeard/svelte-heroicons/solid/ArrowTrendingUp" import Searchbar from "./Base/Searchbar.svelte" + import ChevronDoubleRight from "@babeard/svelte-heroicons/mini/ChevronDoubleRight" const featureSwitches = new OsmConnectionFeatureSwitches() const osmConnection = new OsmConnection({ @@ -40,27 +36,71 @@ }) const state = new UserRelatedState(osmConnection) const t = Translations.t.index + const tu = Translations.t.general const tr = Translations.t.general.morescreen let userLanguages = osmConnection.userDetails.map((ud) => ud.languages) - let themeSearchText: UIEventSource = new UIEventSource("") + let search: UIEventSource = new UIEventSource("") + let searchStable = search.stabilized(100) + const officialThemes: MinimalLayoutInformation[] = MoreScreen.officialThemes.themes.filter(th => th.hideFromOverview === false) + const hiddenThemes: MinimalLayoutInformation[] = MoreScreen.officialThemes.themes.filter(th => th.hideFromOverview === true) + let visitedHiddenThemes: Store = MoreScreen.knownHiddenThemes(state.osmConnection) + .map((knownIds) => hiddenThemes.filter((theme) => + knownIds.has(theme.id) || state.osmConnection.userDetails.data.name === "Pieter Vander Vennet" + )) + + + const customThemes: Store = Stores.ListStabilized(state.installedUserThemes) + .mapD(stableIds => stableIds.map(id => state.getUnofficialTheme(id))) + + + function filtered(themes: MinimalLayoutInformation[]): Store { + return searchStable.map(search => { + if (!search) { + return themes + } + const scores = MoreScreen.sortedByLowest(search, themes) + 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) + }) + } + + + let officialSearched = filtered(officialThemes) + let hiddenSearched = visitedHiddenThemes.bindD(visited => filtered(visited)) + let customSearched = customThemes.bindD(customThemes => filtered(customThemes)) + + + let searchIsFocussed = new UIEventSource(false) document.addEventListener("keydown", function(event) { if (event.ctrlKey && event.code === "KeyF") { - document.getElementById("theme-search")?.focus() + searchIsFocussed.set(true) event.preventDefault() } }) - let visitedHiddenThemes: Store - const hiddenThemes: LayoutInformation[] = - (themeOverview["default"] ?? themeOverview)?.filter((layout) => layout.hideFromOverview) ?? [] - { - visitedHiddenThemes = MoreScreen.knownHiddenThemes(state.osmConnection) - .map((knownIds) => hiddenThemes.filter((theme) => - knownIds.has(theme.id) || state.osmConnection.userDetails.data.name === "Pieter Vander Vennet" - )) + function applySearch() { + const didRedirect = MoreScreen.applySearch(search.data) + console.log("Did redirect?", didRedirect) + if (didRedirect) { + // Just for style and readability; won't _actually_ reach this + return + } + + const candidate = officialSearched.data[0] ?? hiddenSearched.data[0] ?? customSearched.data[0] + if (!candidate) { + return + } + + window.location.href = MoreScreen.createUrlFor(candidate) + } + +
@@ -92,20 +132,19 @@ - MoreScreen.applySearch(themeSearchText.data)}/> + applySearch()} isFocused={searchIsFocussed} /> - +

@@ -122,7 +161,19 @@ - + {#if $customThemes.length > 0} + + +

+ +

+ +
+
+ {/if} + let _value = value.data ?? "" @@ -23,8 +24,8 @@ if (focussed) { requestAnimationFrame(() => { if (document.activeElement !== inputElement) { - inputElement.focus() - inputElement.select() + inputElement?.focus() + inputElement?.select() } }) } @@ -38,15 +39,17 @@ on:submit|preventDefault={() => dispatch("search")} >
+ {/if} diff --git a/src/UI/BigComponents/MoreScreen.ts b/src/UI/BigComponents/MoreScreen.ts index 3101346b8..91d8b3d91 100644 --- a/src/UI/BigComponents/MoreScreen.ts +++ b/src/UI/BigComponents/MoreScreen.ts @@ -3,26 +3,37 @@ import { Store } from "../../Logic/UIEventSource" import { Utils } from "../../Utils" import themeOverview from "../../assets/generated/theme_overview.json" import Locale from "../i18n/Locale" -import { Translatable } from "../../Models/ThemeConfig/Json/Translatable" -import { TagRenderingConfigJson } from "../../Models/ThemeConfig/Json/TagRenderingConfigJson" import { OsmConnection } from "../../Logic/Osm/OsmConnection" +export type ThemeSearchScore = { + theme: MinimalLayoutInformation, + lowest: number, + perLayer?: Record, + other: number +} export default class MoreScreen { - public static readonly officialThemes: MinimalLayoutInformation[] = themeOverview + public static readonly officialThemes: { + themes: MinimalLayoutInformation[], + layers: Record> + } = themeOverview public static readonly officialThemesById: Map = new Map() static { - for (const th of MoreScreen.officialThemes) { + for (const th of MoreScreen.officialThemes.themes) { MoreScreen.officialThemesById.set(th.id, th) } } - public static applySearch(searchTerm: string) { + /** 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, ) { searchTerm = searchTerm.toLowerCase() if (!searchTerm) { - return + return false } if (searchTerm === "personal") { - window.location.href = MoreScreen.createUrlFor({ id: "personal" }, false) + window.location.href = MoreScreen.createUrlFor({ id: "personal" }) } if (searchTerm === "bugs" || searchTerm === "issues") { window.location.href = "https://github.com/pietervdvn/MapComplete/issues" @@ -39,77 +50,110 @@ export default class MoreScreen { if (searchTerm === "studio") { window.location.href = "./studio.html" } - // Enter pressed -> search the first _official_ matchin theme and open it - const publicTheme = MoreScreen.officialThemes.find( - (th) => - th.hideFromOverview == false && - th.id !== "personal" && - MoreScreen.MatchesLayout(th, searchTerm), - ) - if (publicTheme !== undefined) { - window.location.href = MoreScreen.createUrlFor(publicTheme, false) - } - const hiddenTheme = MoreScreen.officialThemes.find( - (th) => th.id !== "personal" && MoreScreen.MatchesLayout(th, searchTerm), - ) - if (hiddenTheme !== undefined) { - window.location.href = MoreScreen.createUrlFor(hiddenTheme, false) - } + return false + } - public static MatchesLayout( - layout: MinimalLayoutInformation, - search: string, - language?: string, - ): boolean { - if (search === undefined) { - return true - } - search = Utils.simplifyStringForSearch(search.toLocaleLowerCase()) // See #1729 - if (search.length > 3 && layout.id.toLowerCase().indexOf(search) >= 0) { - return true - } - if (layout.id === "personal") { - return false - } - if (Utils.simplifyStringForSearch(layout.id) === Utils.simplifyStringForSearch(search)) { - return true + /** + * 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[], language?: string): number { + if(!keywords){ + return Infinity } 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(" ")) - const entitiesToSearch: (string | Record | Record)[] = [layout.shortDescription, layout.title, layout.keywords] - for (const entity of entitiesToSearch) { - if (entity === undefined) { - continue - } - - let term: string[] - if (typeof entity === "string") { - term = [entity] - } else { - const terms = [].concat(entity["*"], entity[language]) - if (Array.isArray(terms)) { - term = terms - } else { - term = [terms] + 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 + } + return distanceSummed + } - const minLevehnstein = Math.min(...Utils.NoNull(term).map(t => Utils.levenshteinDistance(search, - Utils.simplifyStringForSearch(t).slice(0, search.length)))) + public static scoreLayers(query: string): Record { + const result: Record = {} + for (const id in this.officialThemes.layers) { + const keywords = this.officialThemes.layers[id] + const distance = this.scoreKeywords(query, keywords) + result[id] = distance + } + return result + } - if (minLevehnstein < 1 || minLevehnstein / search.length < 0.2) { - return true + + public static scoreThemes(query: string, themes: MinimalLayoutInformation[], ignoreLayers: string[] = []): Record { + 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 = {} + 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(this.scoreKeywords(query, keywords), this.scoreKeywords(query, layoutInfo.keywords)) + const lowest = Math.min(other, ...Object.values(perLayer)) + results[theme] = { + theme:layoutInfo, + perLayer, + other, + lowest } } + return results + } - return false + 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 } public static createUrlFor( layout: { id: string }, - isCustom: boolean, - state?: { layoutToUse?: { id } }, + state?: { layoutToUse?: { id } } ): string { if (layout === undefined) { return undefined @@ -136,7 +180,7 @@ export default class MoreScreen { linkPrefix = `${path}/theme.html?layout=${layout.id}&` } - if (isCustom) { + if (layout.id.startsWith("http://") || layout.id.startsWith("https://")) { linkPrefix = `${path}/theme.html?userlayout=${layout.id}&` } @@ -155,7 +199,7 @@ export default class MoreScreen { new Set( Object.keys(preferences) .filter((key) => key.startsWith(prefix)) - .map((key) => key.substring(prefix.length, key.length - "-enabled".length)), + .map((key) => key.substring(prefix.length, key.length - "-enabled".length)) )) } } diff --git a/src/UI/BigComponents/ThemeButton.svelte b/src/UI/BigComponents/ThemeButton.svelte index 2a71fcc1d..7acb3ad3a 100644 --- a/src/UI/BigComponents/ThemeButton.svelte +++ b/src/UI/BigComponents/ThemeButton.svelte @@ -1,31 +1,14 @@ -{#if theme.id !== personal.id || $unlockedPersonal} - - {#if selected} - - {/if} + -{/if} diff --git a/src/UI/BigComponents/ThemesList.svelte b/src/UI/BigComponents/ThemesList.svelte index 24dc4552b..e59eae600 100644 --- a/src/UI/BigComponents/ThemesList.svelte +++ b/src/UI/BigComponents/ThemesList.svelte @@ -4,46 +4,36 @@ import { OsmConnection } from "../../Logic/Osm/OsmConnection" import { UIEventSource } from "../../Logic/UIEventSource" import ThemeButton from "./ThemeButton.svelte" - import { LayoutInformation, MinimalLayoutInformation } from "../../Models/ThemeConfig/LayoutConfig" - import MoreScreen from "./MoreScreen" - import themeOverview from "../../assets/generated/theme_overview.json" + import { MinimalLayoutInformation } from "../../Models/ThemeConfig/LayoutConfig" + import Translations from "../i18n/Translations" + import Tr from "../Base/Tr.svelte" export let search: UIEventSource export let themes: MinimalLayoutInformation[] export let state: { osmConnection: OsmConnection } - export let isCustom: boolean = false - export let hideThemes: boolean = true - // Filter theme based on search value - $: filteredThemes = themes.filter((theme) => MoreScreen.MatchesLayout(theme, $search)) + export let hasSelection : boolean = true - // Determine which is the first theme, after the search, using all themes - $: allFilteredThemes = themeOverview.filter((theme) => MoreScreen.MatchesLayout(theme, $search)) - $: firstTheme = allFilteredThemes[0]
- {#each filteredThemes as theme (theme.id)} - {#if theme !== undefined && !(hideThemes && theme?.hideFromOverview)} - - {#if theme === firstTheme && !isCustom && $search !== "" && $search !== undefined} - - {:else} - + {#each themes as theme (theme.id)} + + {#if $search && hasSelection && themes[0] === theme} + {/if} - {/if} + {/each}
- {#if filteredThemes.length === 0} + {#if themes.length === 0} {/if}
diff --git a/src/UI/BigComponents/UnofficialThemeList.svelte b/src/UI/BigComponents/UnofficialThemeList.svelte index 5667b7203..4d0322523 100644 --- a/src/UI/BigComponents/UnofficialThemeList.svelte +++ b/src/UI/BigComponents/UnofficialThemeList.svelte @@ -12,21 +12,8 @@ osmConnection: OsmConnection } - const t = Translations.t.general - const currentIds: Store = state.installedUserThemes - const stableIds = Stores.ListStabilized(currentIds) + let customThemes - $: customThemes = Utils.NoNull($stableIds.map((id) => state.GetUnofficialTheme(id))) - $: console.log("Custom themes are", customThemes) -{#if customThemes.length > 0} - - -

- -

- -
-
-{/if} + diff --git a/src/UI/Search/SearchResults.svelte b/src/UI/Search/SearchResults.svelte index 7f8b6eb36..a64d0bf8d 100644 --- a/src/UI/Search/SearchResults.svelte +++ b/src/UI/Search/SearchResults.svelte @@ -29,8 +29,6 @@
-

Search results

- {#if $searchTerm.length > 0 && $filterResults.length > 0} @@ -79,7 +77,7 @@

Other maps

- {#each $themeResults as entry} + {#each $themeResults as entry (entry.id)} {/each} diff --git a/src/UI/ThemeViewGUI.svelte b/src/UI/ThemeViewGUI.svelte index 7750c1aa5..1e3771893 100644 --- a/src/UI/ThemeViewGUI.svelte +++ b/src/UI/ThemeViewGUI.svelte @@ -47,14 +47,34 @@ import { CloseButton } from "flowbite-svelte" import Hash from "../Logic/Web/Hash" import Searchbar from "./Base/Searchbar.svelte" + import ChevronRight from "@babeard/svelte-heroicons/mini/ChevronRight" + import ChevronLeft from "@babeard/svelte-heroicons/solid/ChevronLeft" export let state: ThemeViewState + + let layout = state.layout let maplibremap: UIEventSource = state.map let state_selectedElement = state.selectedElement let selectedElement: UIEventSource = new UIEventSource(undefined) let compass = Orientation.singleton.alpha let compassLoaded = Orientation.singleton.gotMeasurement + let hash = Hash.hash + let previewedImage = state.previewedImage + let addNewFeatureMode = state.userRelatedState.addNewFeatureMode + let gpsAvailable = state.geolocation.geolocationState.gpsAvailable + let gpsButtonAriaLabel = state.geolocation.geolocationState.gpsStateExplanation + let debug = state.featureSwitches.featureSwitchIsDebugging + let featureSwitches: FeatureSwitchState = state.featureSwitches + let currentViewLayer: LayerConfig = layout.layers.find((l) => l.id === "current_view") + let rasterLayer: Store = state.mapProperties.rasterLayer + let currentZoom = state.mapProperties.zoom + let showCrosshair = state.userRelatedState.showCrosshair + let visualFeedback = state.visualFeedback + let viewport: UIEventSource = new UIEventSource(undefined) + let mapproperties: MapProperties = state.mapProperties + let searchOpened = state.searchState.showSearchDrawer + Orientation.singleton.startMeasurements() state.selectedElement.addCallback((selected) => { @@ -73,6 +93,8 @@ }) }) + state.mapProperties.installCustomKeyboardHandler(viewport) + let selectedLayer: Store = state.selectedElement.mapD((element) => { if (element.properties.id.startsWith("current_view")) { @@ -80,21 +102,35 @@ } return state.getMatchingLayer(element.properties) }) - let currentZoom = state.mapProperties.zoom - let showCrosshair = state.userRelatedState.showCrosshair - let visualFeedback = state.visualFeedback - let viewport: UIEventSource = new UIEventSource(undefined) - let mapproperties: MapProperties = state.mapProperties - state.mapProperties.installCustomKeyboardHandler(viewport) + let canZoomIn = mapproperties.maxzoom.map( (mz) => mapproperties.zoom.data < mz, - [mapproperties.zoom], + [mapproperties.zoom] ) let canZoomOut = mapproperties.minzoom.map( (mz) => mapproperties.zoom.data > mz, - [mapproperties.zoom], + [mapproperties.zoom] ) + let rasterLayerName = + rasterLayer.data?.properties?.name ?? + AvailableRasterLayers.defaultBackgroundLayer.properties.name + onDestroy( + rasterLayer.addCallbackAndRunD((l) => { + rasterLayerName = l.properties.name + }) + ) + + + debug.addCallbackAndRun((dbg) => { + if (dbg) { + document.body.classList.add("debug") + } else { + document.body.classList.remove("debug") + } + }) + + function updateViewport() { const rect = viewport.data?.getBoundingClientRect() if (!rect) { @@ -108,7 +144,7 @@ const bottomRight = mlmap.unproject([rect.right, rect.bottom]) const bbox = new BBox([ [topLeft.lng, topLeft.lat], - [bottomRight.lng, bottomRight.lat], + [bottomRight.lng, bottomRight.lat] ]) state.visualFeedbackViewportBounds.setData(bbox) } @@ -119,31 +155,6 @@ mapproperties.bounds.addCallbackAndRunD(() => { updateViewport() }) - let featureSwitches: FeatureSwitchState = state.featureSwitches - let currentViewLayer: LayerConfig = layout.layers.find((l) => l.id === "current_view") - let rasterLayer: Store = state.mapProperties.rasterLayer - let rasterLayerName = - rasterLayer.data?.properties?.name ?? - AvailableRasterLayers.defaultBackgroundLayer.properties.name - onDestroy( - rasterLayer.addCallbackAndRunD((l) => { - rasterLayerName = l.properties.name - }), - ) - let previewedImage = state.previewedImage - let addNewFeatureMode = state.userRelatedState.addNewFeatureMode - let gpsAvailable = state.geolocation.geolocationState.gpsAvailable - let gpsButtonAriaLabel = state.geolocation.geolocationState.gpsStateExplanation - let debug = state.featureSwitches.featureSwitchIsDebugging - - - debug.addCallbackAndRun((dbg) => { - if (dbg) { - document.body.classList.add("debug") - } else { - document.body.classList.remove("debug") - } - }) function forwardEventToMap(e: KeyboardEvent) { const mlmap = state.map.data @@ -157,7 +168,6 @@ animation?.cameraAnimation(mlmap) } - let hash = Hash.hash
@@ -303,20 +313,11 @@ -
-
-
- state.searchState.showSearchDrawer.set(false)} /> -
-
- -
+
- - - +
-
- +
+
+ +
+ { + if(searchOpened.data){ + searchOpened.set(false) + }else{ + state.searchState.searchIsFocused.set(true) + } + }}> + +
+
diff --git a/src/Utils.ts b/src/Utils.ts index 9708515f7..73e1c6efc 100644 --- a/src/Utils.ts +++ b/src/Utils.ts @@ -1287,6 +1287,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be return withDistance.map((n) => n[0]) } + public static levenshteinDistance(str1: string, str2: string): number { const track: number[][] = Array(str2.length + 1) .fill(null) @@ -1437,6 +1438,14 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be return d } + public static asRecord(keys: K[], f: ((k: K) => V)): Record { + const results = > {} + for (const key of keys) { + results[key] = f(key) + } + return results + } + static toIdRecord(ts: T[]): Record { const result: Record = {} for (const t of ts) { @@ -1781,7 +1790,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be public static NoNullInplace(items: T[]): T[] { for (let i = items.length - 1; i >= 0; i--) { - if (items[i] === null || items[i] === undefined) { + if (items[i] === null || items[i] === undefined || items[i] === "") { items.splice(i, 1) } }