forked from MapComplete/MapComplete
		
	Search: refactoring searching for themes, refactor allThemesGui, incidentally fix #1679
This commit is contained in:
		
							parent
							
								
									9b8c300e77
								
							
						
					
					
						commit
						d90b6d82d0
					
				
					 18 changed files with 421 additions and 334 deletions
				
			
		|  | @ -4,14 +4,14 @@ | ||||||
|     "en": "Changes made with MapComplete", |     "en": "Changes made with MapComplete", | ||||||
|     "de": "Änderungen mit MapComplete vorgenommen" |     "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": { |   "shortDescription": { | ||||||
|     "en": "Shows changes made by MapComplete", |     "en": "Shows changes made by MapComplete", | ||||||
|     "de": "Änderungen von MapComplete anzeigen" |     "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", |   "icon": "./assets/svg/logo.svg", | ||||||
|   "hideFromOverview": true, |   "hideFromOverview": true, | ||||||
|   "startLat": 0, |   "startLat": 0, | ||||||
|  |  | ||||||
|  | @ -1413,11 +1413,6 @@ input[type="range"].range-lg::-moz-range-thumb { | ||||||
|   margin-right: auto; |   margin-right: auto; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .mx-3 { |  | ||||||
|   margin-left: 0.75rem; |  | ||||||
|   margin-right: 0.75rem; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .my-4 { | .my-4 { | ||||||
|   margin-top: 1rem; |   margin-top: 1rem; | ||||||
|   margin-bottom: 1rem; |   margin-bottom: 1rem; | ||||||
|  | @ -1511,8 +1506,8 @@ input[type="range"].range-lg::-moz-range-thumb { | ||||||
|   margin-left: 0.5rem; |   margin-left: 0.5rem; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .ml-4 { | .mr-3 { | ||||||
|   margin-left: 1rem; |   margin-right: 0.75rem; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .mb-2 { | .mb-2 { | ||||||
|  | @ -1531,6 +1526,10 @@ input[type="range"].range-lg::-moz-range-thumb { | ||||||
|   margin-bottom: 0.25rem; |   margin-bottom: 0.25rem; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | .ml-4 { | ||||||
|  |   margin-left: 1rem; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| .mt-8 { | .mt-8 { | ||||||
|   margin-top: 2rem; |   margin-top: 2rem; | ||||||
| } | } | ||||||
|  | @ -3913,6 +3912,11 @@ input[type="range"].range-lg::-moz-range-thumb { | ||||||
|   padding-bottom: 0.25rem; |   padding-bottom: 0.25rem; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | .px-0 { | ||||||
|  |   padding-left: 0px; | ||||||
|  |   padding-right: 0px; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| .px-3 { | .px-3 { | ||||||
|   padding-left: 0.75rem; |   padding-left: 0.75rem; | ||||||
|   padding-right: 0.75rem; |   padding-right: 0.75rem; | ||||||
|  | @ -3978,11 +3982,6 @@ input[type="range"].range-lg::-moz-range-thumb { | ||||||
|   padding-bottom: 0.875rem; |   padding-bottom: 0.875rem; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .px-0 { |  | ||||||
|   padding-left: 0px; |  | ||||||
|   padding-right: 0px; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .\!px-0 { | .\!px-0 { | ||||||
|   padding-left: 0px !important; |   padding-left: 0px !important; | ||||||
|   padding-right: 0px !important; |   padding-right: 0px !important; | ||||||
|  | @ -4005,10 +4004,6 @@ input[type="range"].range-lg::-moz-range-thumb { | ||||||
|   padding-left: 0.5rem; |   padding-left: 0.5rem; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .pl-1 { |  | ||||||
|   padding-left: 0.25rem; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .pr-1 { | .pr-1 { | ||||||
|   padding-right: 0.25rem; |   padding-right: 0.25rem; | ||||||
| } | } | ||||||
|  | @ -4037,6 +4032,10 @@ input[type="range"].range-lg::-moz-range-thumb { | ||||||
|   padding-right: 1rem; |   padding-right: 1rem; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | .pl-1 { | ||||||
|  |   padding-left: 0.25rem; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| .pb-1\.5 { | .pb-1\.5 { | ||||||
|   padding-bottom: 0.375rem; |   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)); |   color: rgb(200 30 30 / var(--tw-placeholder-opacity)); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | .accent-gray-600 { | ||||||
|  |   accent-color: #4B5563; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| .opacity-50 { | .opacity-50 { | ||||||
|   opacity: 0.5; |   opacity: 0.5; | ||||||
| } | } | ||||||
|  | @ -8113,10 +8116,6 @@ svg.apply-fill path { | ||||||
|     margin-right: 0.25rem; |     margin-right: 0.25rem; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   .sm\:mt-0 { |  | ||||||
|     margin-top: 0px; |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   .sm\:mt-2 { |   .sm\:mt-2 { | ||||||
|     margin-top: 0.5rem; |     margin-top: 0.5rem; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  | @ -29,13 +29,12 @@ import LayerConfig from "../src/Models/ThemeConfig/LayerConfig" | ||||||
| import PointRenderingConfig from "../src/Models/ThemeConfig/PointRenderingConfig" | import PointRenderingConfig from "../src/Models/ThemeConfig/PointRenderingConfig" | ||||||
| import { ConversionContext } from "../src/Models/ThemeConfig/Conversion/ConversionContext" | import { ConversionContext } from "../src/Models/ThemeConfig/Conversion/ConversionContext" | ||||||
| import { GenerateFavouritesLayer } from "./generateFavouritesLayer" | 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 Translations from "../src/UI/i18n/Translations" | ||||||
| import { Translatable } from "../src/Models/ThemeConfig/Json/Translatable" | import { Translatable } from "../src/Models/ThemeConfig/Json/Translatable" | ||||||
| import { ValidateThemeAndLayers } from "../src/Models/ThemeConfig/Conversion/ValidateThemeAndLayers" | import { ValidateThemeAndLayers } from "../src/Models/ThemeConfig/Conversion/ValidateThemeAndLayers" | ||||||
| import { ExtractImages } from "../src/Models/ThemeConfig/Conversion/FixImages" | import { ExtractImages } from "../src/Models/ThemeConfig/Conversion/FixImages" | ||||||
| import { | import { | ||||||
|     MinimalTagRenderingConfigJson, |  | ||||||
|     TagRenderingConfigJson, |     TagRenderingConfigJson, | ||||||
| } from "../src/Models/ThemeConfig/Json/TagRenderingConfigJson" | } from "../src/Models/ThemeConfig/Json/TagRenderingConfigJson" | ||||||
| 
 | 
 | ||||||
|  | @ -189,7 +188,7 @@ class LayerOverviewUtils extends Script { | ||||||
|         return publicLayerIds |         return publicLayerIds | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public static cleanTranslation(t: Record<string, string> | Translation): Translatable { |     public static cleanTranslation(t: string | Record<string, string> | Translation): Translatable { | ||||||
|         return Translations.T(t).OnEveryLanguage((s) => parse_html(s).textContent).translations |         return Translations.T(t).OnEveryLanguage((s) => parse_html(s).textContent).translations | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | @ -212,25 +211,17 @@ class LayerOverviewUtils extends Script { | ||||||
|         return false |         return false | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     writeSmallOverview( |     static mergeKeywords(into: Record<string, string[]>, source: Readonly<Record<string, string[]>>){ | ||||||
|         themes: { |         for (const key in source) { | ||||||
|             id: string |             if(into[key]){ | ||||||
|             title: any |                 into[key].push(...source[key]) | ||||||
|             shortDescription: any |             }else{ | ||||||
|             icon: string |                 into[key] = source[key] | ||||||
|             hideFromOverview: boolean |  | ||||||
|             mustHaveLanguage: boolean |  | ||||||
|             layers: ( |  | ||||||
|                 | LayerConfigJson |  | ||||||
|                 | string |  | ||||||
|                 | { |  | ||||||
|                 builtin |  | ||||||
|             } |             } | ||||||
|                 )[] |         } | ||||||
|         }[], |     } | ||||||
|     ) { | 
 | ||||||
|         const perId = new Map<string, any>() |     private layerKeywords(l: LayerConfigJson): Record<string, string[]> { | ||||||
|         for (const theme of themes) { |  | ||||||
|         const keywords: Record<string, string[]> = {} |         const keywords: Record<string, string[]> = {} | ||||||
| 
 | 
 | ||||||
|         function addWord(language: string, word: string | string[]) { |         function addWord(language: string, word: string | string[]) { | ||||||
|  | @ -243,7 +234,6 @@ class LayerOverviewUtils extends Script { | ||||||
|             if(!word){ |             if(!word){ | ||||||
|                 return |                 return | ||||||
|             } |             } | ||||||
|                 console.log(language, "--->", word) |  | ||||||
|             if (!keywords[language]) { |             if (!keywords[language]) { | ||||||
|                 keywords[language] = [] |                 keywords[language] = [] | ||||||
|             } |             } | ||||||
|  | @ -261,7 +251,7 @@ class LayerOverviewUtils extends Script { | ||||||
|             if (tr["render"] !== undefined || tr["mappings"] !== undefined) { |             if (tr["render"] !== undefined || tr["mappings"] !== undefined) { | ||||||
|                 tr = <TagRenderingConfigJson>tr |                 tr = <TagRenderingConfigJson>tr | ||||||
|                 addWords(<Translatable>tr.render) |                 addWords(<Translatable>tr.render) | ||||||
|                     for (let mapping of tr.mappings ?? []) { |                 for (const mapping of tr.mappings ?? []) { | ||||||
|                     if (typeof mapping === "string") { |                     if (typeof mapping === "string") { | ||||||
|                         addWords(mapping) |                         addWords(mapping) | ||||||
|                         continue |                         continue | ||||||
|  | @ -274,16 +264,54 @@ class LayerOverviewUtils extends Script { | ||||||
|                 addWord(lang, tr[lang]) |                 addWord(lang, tr[lang]) | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
| 
 |  | ||||||
|             for (const layer of theme.layers ?? []) { |  | ||||||
|                 const l = <LayerConfigJson>layer |  | ||||||
|         addWord("*", l.id) |         addWord("*", l.id) | ||||||
|         addWords(l.title) |         addWords(l.title) | ||||||
|         addWords(l.description) |         addWords(l.description) | ||||||
|         addWords(l.searchTerms) |         addWords(l.searchTerms) | ||||||
|  |         return keywords | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|             const data = { |     writeSmallOverview( | ||||||
|  |         themes: { | ||||||
|  |             id: string | ||||||
|  |             title: Translatable | ||||||
|  |             shortDescription: Translatable | ||||||
|  |             icon: string | ||||||
|  |             hideFromOverview: boolean | ||||||
|  |             mustHaveLanguage: boolean | ||||||
|  |             layers: ( | ||||||
|  |                 | LayerConfigJson | ||||||
|  |                 | string | ||||||
|  |                 | { | ||||||
|  |                 builtin | ||||||
|  |             } | ||||||
|  |                 )[] | ||||||
|  |         }[], | ||||||
|  |         sharedLayers: Map<string, LayerConfigJson> | ||||||
|  |     ) { | ||||||
|  |         const layerKeywords : Record<string, Record<string, string[]>> = {} | ||||||
|  | 
 | ||||||
|  |         sharedLayers.forEach((layer, id) => { | ||||||
|  |             layerKeywords[id] =  this.layerKeywords(layer) | ||||||
|  |         }) | ||||||
|  | 
 | ||||||
|  |         const perId = new Map<string, MinimalLayoutInformation>() | ||||||
|  |         for (const theme of themes) { | ||||||
|  | 
 | ||||||
|  |             const keywords: Record<string, string[]> = {} | ||||||
|  |             for (const layer of theme.layers ?? []) { | ||||||
|  |                 const l = <LayerConfigJson>layer | ||||||
|  |                 if(sharedLayers.has(l.id)){ | ||||||
|  |                     continue | ||||||
|  |                 } | ||||||
|  |                 if(l.id.startsWith("note_import")){ | ||||||
|  |                     continue | ||||||
|  |                 } | ||||||
|  |                 LayerOverviewUtils.mergeKeywords(keywords, this.layerKeywords(l)) | ||||||
|  | 
 | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             const data = <MinimalLayoutInformation> { | ||||||
|                 id: theme.id, |                 id: theme.id, | ||||||
|                 title: theme.title, |                 title: theme.title, | ||||||
|                 shortDescription: LayerOverviewUtils.cleanTranslation(theme.shortDescription), |                 shortDescription: LayerOverviewUtils.cleanTranslation(theme.shortDescription), | ||||||
|  | @ -291,6 +319,7 @@ 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"]) | ||||||
|             } |             } | ||||||
|             perId.set(theme.id, data) |             perId.set(theme.id, data) | ||||||
|         } |         } | ||||||
|  | @ -311,7 +340,7 @@ class LayerOverviewUtils extends Script { | ||||||
| 
 | 
 | ||||||
|         writeFileSync( |         writeFileSync( | ||||||
|             "./src/assets/generated/theme_overview.json", |             "./src/assets/generated/theme_overview.json", | ||||||
|             JSON.stringify(sorted, null, "  "), |             JSON.stringify({ layers: layerKeywords, themes: sorted }, null, "  "), | ||||||
|             { encoding: "utf8" }, |             { encoding: "utf8" }, | ||||||
|         ) |         ) | ||||||
|     } |     } | ||||||
|  | @ -927,7 +956,7 @@ class LayerOverviewUtils extends Script { | ||||||
|         if (whitelist.size == 0) { |         if (whitelist.size == 0) { | ||||||
|             this.writeSmallOverview( |             this.writeSmallOverview( | ||||||
|                 Array.from(fixed.values()).map((t) => { |                 Array.from(fixed.values()).map((t) => { | ||||||
|                     return { |                     return <any> { | ||||||
|                         ...t, |                         ...t, | ||||||
|                         hideFromOverview: t.hideFromOverview ?? false, |                         hideFromOverview: t.hideFromOverview ?? false, | ||||||
|                         shortDescription: |                         shortDescription: | ||||||
|  | @ -935,6 +964,7 @@ class LayerOverviewUtils extends Script { | ||||||
|                         mustHaveLanguage: t.mustHaveLanguage?.length > 0, |                         mustHaveLanguage: t.mustHaveLanguage?.length > 0, | ||||||
|                     } |                     } | ||||||
|                 }), |                 }), | ||||||
|  |                 sharedLayers | ||||||
|             ) |             ) | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -7,11 +7,9 @@ import Constants from "../../Models/Constants" | ||||||
| 
 | 
 | ||||||
| export default class FilterSearch implements GeocodingProvider { | export default class FilterSearch implements GeocodingProvider { | ||||||
|     private readonly _state: SpecialVisualizationState |     private readonly _state: SpecialVisualizationState | ||||||
|     private readonly suggestions |  | ||||||
| 
 | 
 | ||||||
|     constructor(state: SpecialVisualizationState) { |     constructor(state: SpecialVisualizationState) { | ||||||
|         this._state = state |         this._state = state | ||||||
|         this.suggestions = this.getSuggestions() |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     async search(query: string): Promise<SearchResult[]> { |     async search(query: string): Promise<SearchResult[]> { | ||||||
|  | @ -58,7 +56,6 @@ export default class FilterSearch implements GeocodingProvider { | ||||||
|                     Utils.NoNullInplace(terms) |                     Utils.NoNullInplace(terms) | ||||||
|                     const distances = queries.flatMap(query => terms.map(entry => { |                     const distances = queries.flatMap(query => terms.map(entry => { | ||||||
|                         const d = Utils.levenshteinDistance(query, entry.slice(0, query.length)) |                         const d = Utils.levenshteinDistance(query, entry.slice(0, query.length)) | ||||||
|                         console.log(query, "?  +", terms, "=", d) |  | ||||||
|                         const dRelative = d / query.length |                         const dRelative = d / query.length | ||||||
|                         return dRelative |                         return dRelative | ||||||
|                     })) |                     })) | ||||||
|  | @ -79,10 +76,10 @@ export default class FilterSearch implements GeocodingProvider { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  |     /** | ||||||
|  |      * Create a random list of filters | ||||||
|  |      */ | ||||||
|     getSuggestions(): FilterPayload[] { |     getSuggestions(): FilterPayload[] { | ||||||
|         if (this.suggestions) { |  | ||||||
|        //     return this.suggestions
 |  | ||||||
|         } |  | ||||||
|         const result: FilterPayload[] = [] |         const result: FilterPayload[] = [] | ||||||
|         for (const [id, filteredLayer] of this._state.layerState.filteredLayers) { |         for (const [id, filteredLayer] of this._state.layerState.filteredLayers) { | ||||||
|             if (!Array.isArray(filteredLayer.layerDef.filters)) { |             if (!Array.isArray(filteredLayer.layerDef.filters)) { | ||||||
|  |  | ||||||
|  | @ -13,7 +13,6 @@ export class RecentSearch { | ||||||
| 
 | 
 | ||||||
|     constructor(state: { layout: LayoutConfig, osmConnection: OsmConnection, selectedElement: Store<Feature> }) { |     constructor(state: { layout: LayoutConfig, osmConnection: OsmConnection, selectedElement: Store<Feature> }) { | ||||||
|         const prefs = state.osmConnection.preferencesHandler.GetLongPreference("previous-searches") |         const prefs = state.osmConnection.preferencesHandler.GetLongPreference("previous-searches") | ||||||
|         prefs.set(null) |  | ||||||
|         this._seenThisSession = new UIEventSource<GeocodeResult[]>([])//UIEventSource.asObject<GeoCodeResult[]>(prefs, [])
 |         this._seenThisSession = new UIEventSource<GeocodeResult[]>([])//UIEventSource.asObject<GeoCodeResult[]>(prefs, [])
 | ||||||
|         this.seenThisSession = this._seenThisSession |         this.seenThisSession = this._seenThisSession | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -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 * as themeOverview from "../../assets/generated/theme_overview.json" | ||||||
| import { MinimalLayoutInformation } from "../../Models/ThemeConfig/LayoutConfig" | import { MinimalLayoutInformation } from "../../Models/ThemeConfig/LayoutConfig" | ||||||
| import { SpecialVisualizationState } from "../../UI/SpecialVisualization" | import { SpecialVisualizationState } from "../../UI/SpecialVisualization" | ||||||
| import { Utils } from "../../Utils" |  | ||||||
| import MoreScreen from "../../UI/BigComponents/MoreScreen" | import MoreScreen from "../../UI/BigComponents/MoreScreen" | ||||||
| import { ImmutableStore, Store } from "../UIEventSource" | import { ImmutableStore, Store } from "../UIEventSource" | ||||||
| 
 | 
 | ||||||
|  | @ -12,11 +11,16 @@ export default class ThemeSearch implements GeocodingProvider { | ||||||
|     private readonly _state: SpecialVisualizationState |     private readonly _state: SpecialVisualizationState | ||||||
|     private readonly _knownHiddenThemes: Store<Set<string>> |     private readonly _knownHiddenThemes: Store<Set<string>> | ||||||
|     private readonly _suggestionLimit: number |     private readonly _suggestionLimit: number | ||||||
|  |     private readonly _layersToIgnore: string[] | ||||||
|  |     private readonly _otherThemes: MinimalLayoutInformation[] | ||||||
| 
 | 
 | ||||||
|     constructor(state: SpecialVisualizationState, suggestionLimit: number) { |     constructor(state: SpecialVisualizationState, suggestionLimit: number) { | ||||||
|         this._state = state |         this._state = state | ||||||
|  |         this._layersToIgnore = state.layout.layers.map(l => l.id) | ||||||
|         this._suggestionLimit = suggestionLimit |         this._suggestionLimit = suggestionLimit | ||||||
|         this._knownHiddenThemes = MoreScreen.knownHiddenThemes(this._state.osmConnection) |         this._knownHiddenThemes = MoreScreen.knownHiddenThemes(this._state.osmConnection) | ||||||
|  |         this._otherThemes = MoreScreen.officialThemes.themes | ||||||
|  |             .filter(th => th.id !== state.layout.id) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     async search(query: string): Promise<SearchResult[]> { |     async search(query: string): Promise<SearchResult[]> { | ||||||
|  | @ -40,11 +44,11 @@ export default class ThemeSearch implements GeocodingProvider { | ||||||
|         if (query.length < 1) { |         if (query.length < 1) { | ||||||
|             return [] |             return [] | ||||||
|         } |         } | ||||||
|         query = Utils.simplifyStringForSearch(query) |         const sorted = MoreScreen.sortedByLowest(query, this._otherThemes, this._layersToIgnore) | ||||||
|         return ThemeSearch.allThemes |         console.log(">>>", sorted) | ||||||
|  |         return sorted | ||||||
|  |             .map(th => th.theme) | ||||||
|             .filter(th => !th.hideFromOverview || this._knownHiddenThemes.data.has(th.id)) |             .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) |             .slice(0, limit) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -154,7 +154,7 @@ export default class SearchState { | ||||||
|             const poi = result[0] |             const poi = result[0] | ||||||
|             if (poi.category === "theme") { |             if (poi.category === "theme") { | ||||||
|                 const theme = <MinimalLayoutInformation>poi.payload |                 const theme = <MinimalLayoutInformation>poi.payload | ||||||
|                 const url = MoreScreen.createUrlFor(theme, false) |                 const url = MoreScreen.createUrlFor(theme) | ||||||
|                 window.location = <any>url |                 window.location = <any>url | ||||||
|                 return true |                 return true | ||||||
|             } |             } | ||||||
|  |  | ||||||
|  | @ -1,4 +1,4 @@ | ||||||
| import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig" | import LayoutConfig, { MinimalLayoutInformation } from "../../Models/ThemeConfig/LayoutConfig" | ||||||
| import { OsmConnection } from "../Osm/OsmConnection" | import { OsmConnection } from "../Osm/OsmConnection" | ||||||
| import { MangroveIdentity } from "../Web/MangroveReviews" | import { MangroveIdentity } from "../Web/MangroveReviews" | ||||||
| import { Store, Stores, UIEventSource } from "../UIEventSource" | import { Store, Stores, UIEventSource } from "../UIEventSource" | ||||||
|  | @ -143,6 +143,7 @@ export default class UserRelatedState { | ||||||
|         if (layout) { |         if (layout) { | ||||||
|             const osmConn = this.osmConnection |             const osmConn = this.osmConnection | ||||||
|             const recentlyVisited = this._recentlyVisitedThemes |             const recentlyVisited = this._recentlyVisitedThemes | ||||||
|  | 
 | ||||||
|             function update() { |             function update() { | ||||||
|                 if (!osmConn.isLoggedIn.data) { |                 if (!osmConn.isLoggedIn.data) { | ||||||
|                     return |                     return | ||||||
|  | @ -203,16 +204,7 @@ export default class UserRelatedState { | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public GetUnofficialTheme(id: string): |     public getUnofficialTheme(id: string): (MinimalLayoutInformation & { definition }) | undefined { | ||||||
|         | { |  | ||||||
|         id: string |  | ||||||
|         icon: string |  | ||||||
|         title: any |  | ||||||
|         shortDescription: any |  | ||||||
|         definition?: any |  | ||||||
|         isOfficial: boolean |  | ||||||
|     } |  | ||||||
|         | undefined { |  | ||||||
|         const pref = this.osmConnection.GetLongPreference("unofficial-theme-" + id) |         const pref = this.osmConnection.GetLongPreference("unofficial-theme-" + id) | ||||||
|         const str = pref.data |         const str = pref.data | ||||||
| 
 | 
 | ||||||
|  | @ -222,16 +214,7 @@ export default class UserRelatedState { | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         try { |         try { | ||||||
|             const value: { |             return <MinimalLayoutInformation & { definition: string }>JSON.parse(str) | ||||||
|                 id: string |  | ||||||
|                 icon: string |  | ||||||
|                 title: any |  | ||||||
|                 shortDescription: any |  | ||||||
|                 definition?: any |  | ||||||
|                 isOfficial: boolean |  | ||||||
|             } = JSON.parse(str) |  | ||||||
|             value.isOfficial = false |  | ||||||
|             return value |  | ||||||
|         } catch (e) { |         } catch (e) { | ||||||
|             console.warn( |             console.warn( | ||||||
|                 "Removing theme " + |                 "Removing theme " + | ||||||
|  |  | ||||||
|  | @ -22,10 +22,10 @@ export class MinimalLayoutInformation { | ||||||
|     icon: string |     icon: string | ||||||
|     title: Translatable |     title: Translatable | ||||||
|     shortDescription: Translatable |     shortDescription: Translatable | ||||||
|     definition?: Translatable |  | ||||||
|     mustHaveLanguage?: boolean |     mustHaveLanguage?: boolean | ||||||
|     hideFromOverview?: boolean |     hideFromOverview?: boolean | ||||||
|     keywords?: Record<string, string[]> |     keywords?: Record<string, string[]> | ||||||
|  |     layers: string[] | ||||||
| } | } | ||||||
| /** | /** | ||||||
|  * Minimal information about a theme |  * Minimal information about a theme | ||||||
|  |  | ||||||
|  | @ -11,16 +11,11 @@ | ||||||
|   import LoginToggle from "./Base/LoginToggle.svelte" |   import LoginToggle from "./Base/LoginToggle.svelte" | ||||||
|   import Pencil from "../assets/svg/Pencil.svelte" |   import Pencil from "../assets/svg/Pencil.svelte" | ||||||
|   import Constants from "../Models/Constants" |   import Constants from "../Models/Constants" | ||||||
|   import { Store, UIEventSource } from "../Logic/UIEventSource" |   import { Store, Stores, UIEventSource } from "../Logic/UIEventSource" | ||||||
|   import { placeholder } from "../Utils/placeholder" |  | ||||||
|   import { SearchIcon } from "@rgossiaux/svelte-heroicons/solid" |  | ||||||
|   import ThemesList from "./BigComponents/ThemesList.svelte" |   import ThemesList from "./BigComponents/ThemesList.svelte" | ||||||
|   import { LayoutInformation } from "../Models/ThemeConfig/LayoutConfig" |   import { MinimalLayoutInformation } from "../Models/ThemeConfig/LayoutConfig" | ||||||
|   import * as themeOverview from "../assets/generated/theme_overview.json" |  | ||||||
|   import UnofficialThemeList from "./BigComponents/UnofficialThemeList.svelte" |  | ||||||
|   import Eye from "../assets/svg/Eye.svelte" |   import Eye from "../assets/svg/Eye.svelte" | ||||||
|   import LoginButton from "./Base/LoginButton.svelte" |   import LoginButton from "./Base/LoginButton.svelte" | ||||||
|   import ChevronDoubleRight from "@babeard/svelte-heroicons/mini/ChevronDoubleRight" |  | ||||||
|   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" | ||||||
|  | @ -28,6 +23,7 @@ | ||||||
|   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 ChevronDoubleRight from "@babeard/svelte-heroicons/mini/ChevronDoubleRight" | ||||||
| 
 | 
 | ||||||
|   const featureSwitches = new OsmConnectionFeatureSwitches() |   const featureSwitches = new OsmConnectionFeatureSwitches() | ||||||
|   const osmConnection = new OsmConnection({ |   const osmConnection = new OsmConnection({ | ||||||
|  | @ -40,27 +36,71 @@ | ||||||
|   }) |   }) | ||||||
|   const state = new UserRelatedState(osmConnection) |   const state = new UserRelatedState(osmConnection) | ||||||
|   const t = Translations.t.index |   const t = Translations.t.index | ||||||
|  |   const tu = Translations.t.general | ||||||
|   const tr = Translations.t.general.morescreen |   const tr = Translations.t.general.morescreen | ||||||
| 
 | 
 | ||||||
|   let userLanguages = osmConnection.userDetails.map((ud) => ud.languages) |   let userLanguages = osmConnection.userDetails.map((ud) => ud.languages) | ||||||
|   let themeSearchText: UIEventSource<string | undefined> = new UIEventSource<string>("") |   let search: UIEventSource<string | undefined> = new UIEventSource<string>("") | ||||||
|  |   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<MinimalLayoutInformation[]> = MoreScreen.knownHiddenThemes(state.osmConnection) | ||||||
|  |     .map((knownIds) => hiddenThemes.filter((theme) => | ||||||
|  |       knownIds.has(theme.id) || state.osmConnection.userDetails.data.name === "Pieter Vander Vennet" | ||||||
|  |     )) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |   const customThemes: Store<MinimalLayoutInformation[]> = Stores.ListStabilized<string>(state.installedUserThemes) | ||||||
|  |     .mapD(stableIds => stableIds.map(id => state.getUnofficialTheme(id))) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |   function filtered(themes: MinimalLayoutInformation[]): Store<MinimalLayoutInformation[]> { | ||||||
|  |     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) { |   document.addEventListener("keydown", function(event) { | ||||||
|     if (event.ctrlKey && event.code === "KeyF") { |     if (event.ctrlKey && event.code === "KeyF") { | ||||||
|       document.getElementById("theme-search")?.focus() |       searchIsFocussed.set(true) | ||||||
|       event.preventDefault() |       event.preventDefault() | ||||||
|     } |     } | ||||||
|   }) |   }) | ||||||
| 
 | 
 | ||||||
|   let visitedHiddenThemes: Store<LayoutInformation[]> |   function applySearch() { | ||||||
|   const hiddenThemes: LayoutInformation[] = |     const didRedirect = MoreScreen.applySearch(search.data) | ||||||
|     (themeOverview["default"] ?? themeOverview)?.filter((layout) => layout.hideFromOverview) ?? [] |     console.log("Did redirect?", didRedirect) | ||||||
|   { |     if (didRedirect) { | ||||||
|     visitedHiddenThemes = MoreScreen.knownHiddenThemes(state.osmConnection) |       // Just for style and readability; won't _actually_ reach this | ||||||
|       .map((knownIds) => hiddenThemes.filter((theme) => |       return | ||||||
|         knownIds.has(theme.id) || state.osmConnection.userDetails.data.name === "Pieter Vander Vennet" |  | ||||||
|       )) |  | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|  |     const candidate = officialSearched.data[0] ?? hiddenSearched.data[0] ?? customSearched.data[0] | ||||||
|  |     if (!candidate) { | ||||||
|  |       return | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     window.location.href = MoreScreen.createUrlFor(candidate) | ||||||
|  | 
 | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <main> | <main> | ||||||
|  | @ -92,20 +132,19 @@ | ||||||
|       </div> |       </div> | ||||||
|     </div> |     </div> | ||||||
| 
 | 
 | ||||||
|     <Searchbar value={themeSearchText} placeholder={tr.searchForATheme} on:search={() => MoreScreen.applySearch(themeSearchText.data)}/> |     <Searchbar value={search} placeholder={tr.searchForATheme} on:search={() => applySearch()} isFocused={searchIsFocussed} /> | ||||||
| 
 | 
 | ||||||
|     <ThemesList search={themeSearchText} {state} themes={MoreScreen.officialThemes} /> |     <ThemesList {search} {state} themes={$officialSearched} /> | ||||||
| 
 | 
 | ||||||
|     <LoginToggle {state}> |     <LoginToggle {state}> | ||||||
|       <LoginButton clss="primary" {osmConnection} slot="not-logged-in"> |       <LoginButton clss="primary" {osmConnection} slot="not-logged-in"> | ||||||
|         <Tr t={t.logIn} /> |         <Tr t={t.logIn} /> | ||||||
|       </LoginButton> |       </LoginButton> | ||||||
|       <ThemesList |       <ThemesList | ||||||
|         hideThemes={false} |         {search} | ||||||
|         isCustom={false} |  | ||||||
|         search={themeSearchText} |  | ||||||
|         {state} |         {state} | ||||||
|         themes={$visitedHiddenThemes} |         themes={$hiddenSearched} | ||||||
|  |         hasSelection={$officialSearched.length === 0} | ||||||
|       > |       > | ||||||
|         <svelte:fragment slot="title"> |         <svelte:fragment slot="title"> | ||||||
|           <h3> |           <h3> | ||||||
|  | @ -122,7 +161,19 @@ | ||||||
|         </svelte:fragment> |         </svelte:fragment> | ||||||
|       </ThemesList> |       </ThemesList> | ||||||
| 
 | 
 | ||||||
|       <UnofficialThemeList search={themeSearchText} {state} /> |       {#if $customThemes.length > 0} | ||||||
|  |         <ThemesList {search} {state} themes={$customSearched} | ||||||
|  |                     hasSelection={$officialSearched.length === 0 && $hiddenSearched.length === 0} | ||||||
|  |         > | ||||||
|  |           <svelte:fragment slot="title"> | ||||||
|  |             <h3> | ||||||
|  |               <Tr t={tu.customThemeTitle} /> | ||||||
|  |             </h3> | ||||||
|  |             <Tr t={tu.customThemeIntro} /> | ||||||
|  |           </svelte:fragment> | ||||||
|  |         </ThemesList> | ||||||
|  |       {/if} | ||||||
|  | 
 | ||||||
|     </LoginToggle> |     </LoginToggle> | ||||||
| 
 | 
 | ||||||
|     <a |     <a | ||||||
|  |  | ||||||
|  | @ -6,6 +6,7 @@ | ||||||
|   import { SearchIcon } from "@rgossiaux/svelte-heroicons/solid" |   import { SearchIcon } from "@rgossiaux/svelte-heroicons/solid" | ||||||
|   import { ariaLabel } from "../../Utils/ariaLabel" |   import { ariaLabel } from "../../Utils/ariaLabel" | ||||||
|   import { Translation } from "../i18n/Translation" |   import { Translation } from "../i18n/Translation" | ||||||
|  |   import Backspace from "@babeard/svelte-heroicons/outline/Backspace" | ||||||
| 
 | 
 | ||||||
|   export let value: UIEventSource<string> |   export let value: UIEventSource<string> | ||||||
|   let _value = value.data ?? "" |   let _value = value.data ?? "" | ||||||
|  | @ -23,8 +24,8 @@ | ||||||
|     if (focussed) { |     if (focussed) { | ||||||
|       requestAnimationFrame(() => { |       requestAnimationFrame(() => { | ||||||
|         if (document.activeElement !== inputElement) { |         if (document.activeElement !== inputElement) { | ||||||
|           inputElement.focus() |           inputElement?.focus() | ||||||
|           inputElement.select() |           inputElement?.select() | ||||||
|         } |         } | ||||||
|       }) |       }) | ||||||
|     } |     } | ||||||
|  | @ -38,15 +39,17 @@ | ||||||
|   on:submit|preventDefault={() => dispatch("search")} |   on:submit|preventDefault={() => dispatch("search")} | ||||||
| > | > | ||||||
|   <label |   <label | ||||||
|     class="neutral-label normal-background flex w-full items-center rounded-full border-2 border-black box-shadow" |     class="neutral-label normal-background flex w-full items-center rounded-full border border-black box-shadow" | ||||||
|   > |   > | ||||||
|  |     <SearchIcon aria-hidden="true" class="h-8 w-8 ml-2" /> | ||||||
|  | 
 | ||||||
|     <input |     <input | ||||||
|       bind:this={inputElement} |       bind:this={inputElement} | ||||||
|       on:focus={() => {isFocused?.setData(true)}} |       on:focus={() => {isFocused?.setData(true)}} | ||||||
|       on:blur={() => {isFocused?.setData(false)}} |       on:blur={() => {isFocused?.setData(false)}} | ||||||
|       type="search" |       type="search" | ||||||
|       style=" --tw-ring-color: rgb(0 0 0 / 0) !important;" |       style=" --tw-ring-color: rgb(0 0 0 / 0) !important;" | ||||||
|       class="ml-4 pl-1 w-full outline-none border-none" |       class="px-0 ml-1 w-full outline-none border-none" | ||||||
|       on:keypress={(keypr) => { |       on:keypress={(keypr) => { | ||||||
|           return keypr.key === "Enter" ? dispatch("search") : undefined |           return keypr.key === "Enter" ? dispatch("search") : undefined | ||||||
|         }} |         }} | ||||||
|  | @ -54,7 +57,11 @@ | ||||||
|       use:set_placeholder={placeholder} |       use:set_placeholder={placeholder} | ||||||
|       use:ariaLabel={placeholder} |       use:ariaLabel={placeholder} | ||||||
|     /> |     /> | ||||||
|     <SearchIcon aria-hidden="true" class="h-8 w-8 mx-3" /> |  | ||||||
| 
 | 
 | ||||||
|  |     {#if $value.length > 0} | ||||||
|  |       <Backspace on:click={() => value.set("")} color="var(--button-background)" class="w-6 h-6 mr-3 cursor-pointer" /> | ||||||
|  |     {:else} | ||||||
|  |       <div class="w-6 mr-3" /> | ||||||
|  |     {/if} | ||||||
|   </label> |   </label> | ||||||
| </form> | </form> | ||||||
|  |  | ||||||
|  | @ -3,26 +3,37 @@ import { Store } from "../../Logic/UIEventSource" | ||||||
| import { Utils } from "../../Utils" | import { Utils } from "../../Utils" | ||||||
| import themeOverview from "../../assets/generated/theme_overview.json" | import themeOverview from "../../assets/generated/theme_overview.json" | ||||||
| import Locale from "../i18n/Locale" | 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" | import { OsmConnection } from "../../Logic/Osm/OsmConnection" | ||||||
| 
 | 
 | ||||||
|  | export  type ThemeSearchScore = { | ||||||
|  |     theme: MinimalLayoutInformation, | ||||||
|  |     lowest: number, | ||||||
|  |     perLayer?: Record<string, number>, | ||||||
|  |     other: number | ||||||
|  | } | ||||||
| export default class MoreScreen { | export default class MoreScreen { | ||||||
|     public static readonly officialThemes: MinimalLayoutInformation[] = themeOverview |     public static readonly officialThemes: { | ||||||
|  |         themes: MinimalLayoutInformation[], | ||||||
|  |         layers: Record<string, Record<string, string[]>> | ||||||
|  |     } = themeOverview | ||||||
|     public static readonly officialThemesById: Map<string, MinimalLayoutInformation> = new Map<string, MinimalLayoutInformation>() |     public static readonly officialThemesById: Map<string, MinimalLayoutInformation> = new Map<string, MinimalLayoutInformation>() | ||||||
|     static { |     static { | ||||||
|         for (const th of MoreScreen.officialThemes) { |         for (const th of MoreScreen.officialThemes.themes) { | ||||||
|             MoreScreen.officialThemesById.set(th.id, th) |             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() |         searchTerm = searchTerm.toLowerCase() | ||||||
|         if (!searchTerm) { |         if (!searchTerm) { | ||||||
|             return |             return false | ||||||
|         } |         } | ||||||
|         if (searchTerm === "personal") { |         if (searchTerm === "personal") { | ||||||
|             window.location.href = MoreScreen.createUrlFor({ id: "personal" }, false) |             window.location.href = MoreScreen.createUrlFor({ id: "personal" }) | ||||||
|         } |         } | ||||||
|         if (searchTerm === "bugs" || searchTerm === "issues") { |         if (searchTerm === "bugs" || searchTerm === "issues") { | ||||||
|             window.location.href = "https://github.com/pietervdvn/MapComplete/issues" |             window.location.href = "https://github.com/pietervdvn/MapComplete/issues" | ||||||
|  | @ -39,77 +50,110 @@ export default class MoreScreen { | ||||||
|         if (searchTerm === "studio") { |         if (searchTerm === "studio") { | ||||||
|             window.location.href = "./studio.html" |             window.location.href = "./studio.html" | ||||||
|         } |         } | ||||||
|         // Enter pressed -> search the first _official_ matchin theme and open it
 |         return false | ||||||
|         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) |  | ||||||
|         } |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public static MatchesLayout( |     /** | ||||||
|         layout: MinimalLayoutInformation, |      * Searches for the smallest distance in words; will split both the query and the terms | ||||||
|         search: string, |      * | ||||||
|         language?: string, |      * MoreScreen.scoreKeywords("drinking water", {"en": ["A layer with drinking water points"]}, "en") // => 0
 | ||||||
|     ): boolean { |      * MoreScreen.scoreKeywords("waste", {"en": ["A layer with drinking water points"]}, "en") // => 2
 | ||||||
|         if (search === undefined) { |      * | ||||||
|             return true |      */ | ||||||
|         } |     public static scoreKeywords(query: string, keywords: Record<string, string[]> | string[], language?: string): number { | ||||||
|         search = Utils.simplifyStringForSearch(search.toLocaleLowerCase()) // See #1729
 |         if(!keywords){ | ||||||
|         if (search.length > 3 && layout.id.toLowerCase().indexOf(search) >= 0) { |             return Infinity | ||||||
|             return true |  | ||||||
|         } |  | ||||||
|         if (layout.id === "personal") { |  | ||||||
|             return false |  | ||||||
|         } |  | ||||||
|         if (Utils.simplifyStringForSearch(layout.id) === Utils.simplifyStringForSearch(search)) { |  | ||||||
|             return true |  | ||||||
|         } |         } | ||||||
|         language ??= Locale.language.data |         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<string, string> | Record<string, string[]>)[] = [layout.shortDescription, layout.title, layout.keywords] |         let distanceSummed = 0 | ||||||
|         for (const entity of entitiesToSearch) { |         for (let i = 0; i < queryParts.length; i++) { | ||||||
|             if (entity === undefined) { |             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 | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     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 | ||||||
|  |         } | ||||||
|  |         return result | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |     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") { | ||||||
|                 continue |                 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 | ||||||
| 
 | 
 | ||||||
|             let term: string[] |             const keywords =Utils.NoNullInplace( [layoutInfo.shortDescription, layoutInfo.title]) | ||||||
|             if (typeof entity === "string") { |                 .map(item => typeof item === "string" ? item : (item[language] ?? item["*"])) | ||||||
|                 term = [entity] | 
 | ||||||
|             } else { | 
 | ||||||
|                 const terms = [].concat(entity["*"], entity[language]) |             const other = Math.min(this.scoreKeywords(query, keywords), this.scoreKeywords(query, layoutInfo.keywords)) | ||||||
|                 if (Array.isArray(terms)) { |             const lowest = Math.min(other, ...Object.values(perLayer)) | ||||||
|                     term = terms |             results[theme] = { | ||||||
|                 } else { |                 theme:layoutInfo, | ||||||
|                     term = [terms] |                 perLayer, | ||||||
|  |                 other, | ||||||
|  |                 lowest | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
| 
 |         return results | ||||||
|             const minLevehnstein = Math.min(...Utils.NoNull(term).map(t => Utils.levenshteinDistance(search, |  | ||||||
|                 Utils.simplifyStringForSearch(t).slice(0, search.length)))) |  | ||||||
| 
 |  | ||||||
|             if (minLevehnstein < 1 || minLevehnstein / search.length < 0.2) { |  | ||||||
|                 return true |  | ||||||
|             } |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|         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( |     public static createUrlFor( | ||||||
|         layout: { id: string }, |         layout: { id: string }, | ||||||
|         isCustom: boolean, |         state?: { layoutToUse?: { id } } | ||||||
|         state?: { layoutToUse?: { id } }, |  | ||||||
|     ): string { |     ): string { | ||||||
|         if (layout === undefined) { |         if (layout === undefined) { | ||||||
|             return undefined |             return undefined | ||||||
|  | @ -136,7 +180,7 @@ export default class MoreScreen { | ||||||
|             linkPrefix = `${path}/theme.html?layout=${layout.id}&` |             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}&` |             linkPrefix = `${path}/theme.html?userlayout=${layout.id}&` | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|  | @ -155,7 +199,7 @@ export default class MoreScreen { | ||||||
|             new Set<string>( |             new Set<string>( | ||||||
|                 Object.keys(preferences) |                 Object.keys(preferences) | ||||||
|                     .filter((key) => key.startsWith(prefix)) |                     .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)) | ||||||
|             )) |             )) | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -1,31 +1,14 @@ | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
|   import * as personal from "../../../assets/themes/personal/personal.json" |   import { ImmutableStore, Store } from "../../Logic/UIEventSource" | ||||||
|   import { ImmutableStore, Store, UIEventSource } from "../../Logic/UIEventSource" |   import { OsmConnection } from "../../Logic/Osm/OsmConnection" | ||||||
|   import UserDetails, { OsmConnection } from "../../Logic/Osm/OsmConnection" |  | ||||||
|   import Constants from "../../Models/Constants" |  | ||||||
|   import type { MinimalLayoutInformation } from "../../Models/ThemeConfig/LayoutConfig" |   import type { MinimalLayoutInformation } from "../../Models/ThemeConfig/LayoutConfig" | ||||||
|   import Tr from "../Base/Tr.svelte" |   import Tr from "../Base/Tr.svelte" | ||||||
|   import Translations from "../i18n/Translations" |   import Translations from "../i18n/Translations" | ||||||
|   import { LocalStorageSource } from "../../Logic/Web/LocalStorageSource" |  | ||||||
|   import Marker from "../Map/Marker.svelte" |   import Marker from "../Map/Marker.svelte" | ||||||
| 
 | 
 | ||||||
|   export let theme: MinimalLayoutInformation |   export let theme: MinimalLayoutInformation & {isOfficial?: boolean} | ||||||
|   export let isCustom: boolean = false |   let isCustom: boolean = theme.id.startsWith("https://") || theme.id.startsWith("http://") | ||||||
|   export let userDetails: UIEventSource<UserDetails> |  | ||||||
|   export let state: { layoutToUse?: { id: string }; osmConnection: OsmConnection } |   export let state: { layoutToUse?: { id: string }; osmConnection: OsmConnection } | ||||||
|   export let selected: boolean = false |  | ||||||
| 
 |  | ||||||
|   let unlockedPersonal = LocalStorageSource.GetParsed("unlocked_personal_theme", false) |  | ||||||
| 
 |  | ||||||
|   userDetails.addCallbackAndRunD((userDetails) => { |  | ||||||
|     if (!userDetails.loggedIn) { |  | ||||||
|       return |  | ||||||
|     } |  | ||||||
|     if (userDetails.csCount > Constants.userJourney.personalLayoutUnlock) { |  | ||||||
|       unlockedPersonal.setData(true) |  | ||||||
|     } |  | ||||||
|     return true |  | ||||||
|   }) |  | ||||||
| 
 | 
 | ||||||
|   $: title = Translations.T( |   $: title = Translations.T( | ||||||
|     theme.title, |     theme.title, | ||||||
|  | @ -33,7 +16,6 @@ | ||||||
|   ) |   ) | ||||||
|   $: description = Translations.T(theme.shortDescription) |   $: description = Translations.T(theme.shortDescription) | ||||||
| 
 | 
 | ||||||
|   // TODO: Improve this function |  | ||||||
|   function createUrl( |   function createUrl( | ||||||
|     layout: { id: string; definition?: string }, |     layout: { id: string; definition?: string }, | ||||||
|     isCustom: boolean, |     isCustom: boolean, | ||||||
|  | @ -84,19 +66,12 @@ | ||||||
|   let href = createUrl(theme, isCustom, state) |   let href = createUrl(theme, isCustom, state) | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| {#if theme.id !== personal.id || $unlockedPersonal} |  | ||||||
|   <a class="low-interaction my-1 flex w-full items-center text-ellipsis rounded p-1" href={$href}> |   <a class="low-interaction my-1 flex w-full items-center text-ellipsis rounded p-1" href={$href}> | ||||||
|     <Marker icons={theme.icon} size="block h-8 w-8 sm:h-11 sm:w-11 m-1 sm:mx-2 md:mx-4 shrink-0" /> |     <Marker icons={theme.icon} size="block h-8 w-8 sm:h-11 sm:w-11 m-1 sm:mx-2 md:mx-4 shrink-0" /> | ||||||
| 
 | 
 | ||||||
|     <span class="flex flex-col overflow-hidden text-ellipsis text-xl font-bold"> |     <span class="flex flex-col overflow-hidden text-ellipsis text-xl font-bold"> | ||||||
|       <Tr cls="" t={title} /> |       <Tr cls="" t={title} /> | ||||||
|       <Tr cls="subtle text-base" t={description} /> |       <Tr cls="subtle text-base" t={description} /> | ||||||
| 
 |       <slot/> | ||||||
|       {#if selected} |  | ||||||
|         <span class="thanks hidden-on-mobile" aria-hidden="true"> |  | ||||||
|           <Tr t={Translations.t.general.morescreen.enterToOpen} /> |  | ||||||
|         </span> |  | ||||||
|       {/if} |  | ||||||
|     </span> |     </span> | ||||||
|   </a> |   </a> | ||||||
| {/if} |  | ||||||
|  |  | ||||||
|  | @ -4,46 +4,36 @@ | ||||||
|   import { OsmConnection } from "../../Logic/Osm/OsmConnection" |   import { OsmConnection } from "../../Logic/Osm/OsmConnection" | ||||||
|   import { UIEventSource } from "../../Logic/UIEventSource" |   import { UIEventSource } from "../../Logic/UIEventSource" | ||||||
|   import ThemeButton from "./ThemeButton.svelte" |   import ThemeButton from "./ThemeButton.svelte" | ||||||
|   import { LayoutInformation, MinimalLayoutInformation } from "../../Models/ThemeConfig/LayoutConfig" |   import { MinimalLayoutInformation } from "../../Models/ThemeConfig/LayoutConfig" | ||||||
|   import MoreScreen from "./MoreScreen" |   import Translations from "../i18n/Translations" | ||||||
|   import themeOverview from "../../assets/generated/theme_overview.json" |   import Tr from "../Base/Tr.svelte" | ||||||
| 
 | 
 | ||||||
|   export let search: UIEventSource<string> |   export let search: UIEventSource<string> | ||||||
|   export let themes: MinimalLayoutInformation[] |   export let themes: MinimalLayoutInformation[] | ||||||
|   export let state: { osmConnection: OsmConnection } |   export let state: { osmConnection: OsmConnection } | ||||||
|   export let isCustom: boolean = false |  | ||||||
|   export let hideThemes: boolean = true |  | ||||||
| 
 | 
 | ||||||
|   // Filter theme based on search value |   export let hasSelection : boolean = true | ||||||
|   $: filteredThemes = themes.filter((theme) => MoreScreen.MatchesLayout(theme, $search)) |  | ||||||
| 
 | 
 | ||||||
|   // Determine which is the first theme, after the search, using all themes |  | ||||||
|   $: allFilteredThemes = themeOverview.filter((theme) => MoreScreen.MatchesLayout(theme, $search)) |  | ||||||
|   $: firstTheme = allFilteredThemes[0] |  | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <section class="w-full"> | <section class="w-full"> | ||||||
|   <slot name="title" /> |   <slot name="title" /> | ||||||
|   <div class="theme-list my-2 gap-4 md:grid md:grid-flow-row md:grid-cols-2 lg:grid-cols-3"> |   <div class="theme-list my-2 gap-4 md:grid md:grid-flow-row md:grid-cols-2 lg:grid-cols-3"> | ||||||
|     {#each filteredThemes as theme (theme.id)} |     {#each themes as theme (theme.id)} | ||||||
|       {#if theme !== undefined && !(hideThemes && theme?.hideFromOverview)} |  | ||||||
|         <!-- TODO: doesn't work if first theme is hidden --> |  | ||||||
|         {#if theme === firstTheme && !isCustom && $search !== "" && $search !== undefined} |  | ||||||
|       <ThemeButton |       <ThemeButton | ||||||
|         {theme} |         {theme} | ||||||
|             {isCustom} |  | ||||||
|             userDetails={state.osmConnection.userDetails} |  | ||||||
|         {state} |         {state} | ||||||
|             selected={true} |       > | ||||||
|           /> |         {#if $search && hasSelection && themes[0] === theme} | ||||||
|         {:else} |         <span class="thanks hidden-on-mobile" aria-hidden="true"> | ||||||
|           <ThemeButton {theme} {isCustom} userDetails={state.osmConnection.userDetails} {state} /> |           <Tr t={Translations.t.general.morescreen.enterToOpen} /> | ||||||
|         {/if} |         </span> | ||||||
|         {/if} |         {/if} | ||||||
|  |       </ThemeButton> | ||||||
|     {/each} |     {/each} | ||||||
|   </div> |   </div> | ||||||
| 
 | 
 | ||||||
|   {#if filteredThemes.length === 0} |   {#if themes.length === 0} | ||||||
|     <NoThemeResultButton {search} /> |     <NoThemeResultButton {search} /> | ||||||
|   {/if} |   {/if} | ||||||
| </section> | </section> | ||||||
|  |  | ||||||
|  | @ -12,21 +12,8 @@ | ||||||
|     osmConnection: OsmConnection |     osmConnection: OsmConnection | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   const t = Translations.t.general | 
 | ||||||
|   const currentIds: Store<string[]> = state.installedUserThemes |  | ||||||
|   const stableIds = Stores.ListStabilized<string>(currentIds) |  | ||||||
|   let customThemes |   let customThemes | ||||||
|   $: customThemes = Utils.NoNull($stableIds.map((id) => state.GetUnofficialTheme(id))) |  | ||||||
|   $: console.log("Custom themes are", customThemes) |  | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| {#if customThemes.length > 0} | 
 | ||||||
|   <ThemesList {search} {state} themes={customThemes} isCustom={true} hideThemes={false}> |  | ||||||
|     <svelte:fragment slot="title"> |  | ||||||
|       <h3> |  | ||||||
|         <Tr t={t.customThemeTitle} /> |  | ||||||
|       </h3> |  | ||||||
|       <Tr t={t.customThemeIntro} /> |  | ||||||
|     </svelte:fragment> |  | ||||||
|   </ThemesList> |  | ||||||
| {/if} |  | ||||||
|  |  | ||||||
|  | @ -29,8 +29,6 @@ | ||||||
| </script> | </script> | ||||||
| <div class="p-4 low-interaction flex gap-y-2 flex-col"> | <div class="p-4 low-interaction flex gap-y-2 flex-col"> | ||||||
| 
 | 
 | ||||||
|   <h3>Search results</h3> |  | ||||||
| 
 |  | ||||||
|   <ActiveFilters activeFilters={$activeFilters} /> |   <ActiveFilters activeFilters={$activeFilters} /> | ||||||
| 
 | 
 | ||||||
|   {#if $searchTerm.length > 0 && $filterResults.length > 0} |   {#if $searchTerm.length > 0 && $filterResults.length > 0} | ||||||
|  | @ -79,7 +77,7 @@ | ||||||
|       <h3> |       <h3> | ||||||
|         Other maps |         Other maps | ||||||
|       </h3> |       </h3> | ||||||
|       {#each $themeResults as entry} |       {#each $themeResults as entry (entry.id)} | ||||||
|         <ThemeResult {entry} /> |         <ThemeResult {entry} /> | ||||||
|       {/each} |       {/each} | ||||||
|     </SidebarUnit> |     </SidebarUnit> | ||||||
|  |  | ||||||
|  | @ -47,14 +47,34 @@ | ||||||
|   import { CloseButton } from "flowbite-svelte" |   import { CloseButton } from "flowbite-svelte" | ||||||
|   import Hash from "../Logic/Web/Hash" |   import Hash from "../Logic/Web/Hash" | ||||||
|   import Searchbar from "./Base/Searchbar.svelte" |   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 |   export let state: ThemeViewState | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|   let layout = state.layout |   let layout = state.layout | ||||||
|   let maplibremap: UIEventSource<MlMap> = state.map |   let maplibremap: UIEventSource<MlMap> = state.map | ||||||
|   let state_selectedElement = state.selectedElement |   let state_selectedElement = state.selectedElement | ||||||
|   let selectedElement: UIEventSource<Feature> = new UIEventSource<Feature>(undefined) |   let selectedElement: UIEventSource<Feature> = new UIEventSource<Feature>(undefined) | ||||||
|   let compass = Orientation.singleton.alpha |   let compass = Orientation.singleton.alpha | ||||||
|   let compassLoaded = Orientation.singleton.gotMeasurement |   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<RasterLayerPolygon> = state.mapProperties.rasterLayer | ||||||
|  |   let currentZoom = state.mapProperties.zoom | ||||||
|  |   let showCrosshair = state.userRelatedState.showCrosshair | ||||||
|  |   let visualFeedback = state.visualFeedback | ||||||
|  |   let viewport: UIEventSource<HTMLDivElement> = new UIEventSource<HTMLDivElement>(undefined) | ||||||
|  |   let mapproperties: MapProperties = state.mapProperties | ||||||
|  |   let searchOpened = state.searchState.showSearchDrawer | ||||||
|  | 
 | ||||||
|   Orientation.singleton.startMeasurements() |   Orientation.singleton.startMeasurements() | ||||||
| 
 | 
 | ||||||
|   state.selectedElement.addCallback((selected) => { |   state.selectedElement.addCallback((selected) => { | ||||||
|  | @ -73,6 +93,8 @@ | ||||||
|     }) |     }) | ||||||
|   }) |   }) | ||||||
| 
 | 
 | ||||||
|  |   state.mapProperties.installCustomKeyboardHandler(viewport) | ||||||
|  | 
 | ||||||
| 
 | 
 | ||||||
|   let selectedLayer: Store<LayerConfig> = state.selectedElement.mapD((element) => { |   let selectedLayer: Store<LayerConfig> = state.selectedElement.mapD((element) => { | ||||||
|     if (element.properties.id.startsWith("current_view")) { |     if (element.properties.id.startsWith("current_view")) { | ||||||
|  | @ -80,21 +102,35 @@ | ||||||
|     } |     } | ||||||
|     return state.getMatchingLayer(element.properties) |     return state.getMatchingLayer(element.properties) | ||||||
|   }) |   }) | ||||||
|   let currentZoom = state.mapProperties.zoom | 
 | ||||||
|   let showCrosshair = state.userRelatedState.showCrosshair |  | ||||||
|   let visualFeedback = state.visualFeedback |  | ||||||
|   let viewport: UIEventSource<HTMLDivElement> = new UIEventSource<HTMLDivElement>(undefined) |  | ||||||
|   let mapproperties: MapProperties = state.mapProperties |  | ||||||
|   state.mapProperties.installCustomKeyboardHandler(viewport) |  | ||||||
|   let canZoomIn = mapproperties.maxzoom.map( |   let canZoomIn = mapproperties.maxzoom.map( | ||||||
|     (mz) => mapproperties.zoom.data < mz, |     (mz) => mapproperties.zoom.data < mz, | ||||||
|     [mapproperties.zoom], |     [mapproperties.zoom] | ||||||
|   ) |   ) | ||||||
|   let canZoomOut = mapproperties.minzoom.map( |   let canZoomOut = mapproperties.minzoom.map( | ||||||
|     (mz) => mapproperties.zoom.data > mz, |     (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() { |   function updateViewport() { | ||||||
|     const rect = viewport.data?.getBoundingClientRect() |     const rect = viewport.data?.getBoundingClientRect() | ||||||
|     if (!rect) { |     if (!rect) { | ||||||
|  | @ -108,7 +144,7 @@ | ||||||
|     const bottomRight = mlmap.unproject([rect.right, rect.bottom]) |     const bottomRight = mlmap.unproject([rect.right, rect.bottom]) | ||||||
|     const bbox = new BBox([ |     const bbox = new BBox([ | ||||||
|       [topLeft.lng, topLeft.lat], |       [topLeft.lng, topLeft.lat], | ||||||
|       [bottomRight.lng, bottomRight.lat], |       [bottomRight.lng, bottomRight.lat] | ||||||
|     ]) |     ]) | ||||||
|     state.visualFeedbackViewportBounds.setData(bbox) |     state.visualFeedbackViewportBounds.setData(bbox) | ||||||
|   } |   } | ||||||
|  | @ -119,31 +155,6 @@ | ||||||
|   mapproperties.bounds.addCallbackAndRunD(() => { |   mapproperties.bounds.addCallbackAndRunD(() => { | ||||||
|     updateViewport() |     updateViewport() | ||||||
|   }) |   }) | ||||||
|   let featureSwitches: FeatureSwitchState = state.featureSwitches |  | ||||||
|   let currentViewLayer: LayerConfig = layout.layers.find((l) => l.id === "current_view") |  | ||||||
|   let rasterLayer: Store<RasterLayerPolygon> = 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) { |   function forwardEventToMap(e: KeyboardEvent) { | ||||||
|     const mlmap = state.map.data |     const mlmap = state.map.data | ||||||
|  | @ -157,7 +168,6 @@ | ||||||
|     animation?.cameraAnimation(mlmap) |     animation?.cameraAnimation(mlmap) | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   let hash = Hash.hash |  | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <main> | <main> | ||||||
|  | @ -303,19 +313,10 @@ | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|   <DrawerRight shown={state.searchState.showSearchDrawer}> |   <DrawerRight shown={state.searchState.showSearchDrawer}> | ||||||
|     <div class="relative"> |  | ||||||
|       <div class="absolute right-0 top-0 "> |  | ||||||
|         <div class="mr-4 mt-4"> |  | ||||||
|           <CloseButton on:click={() => state.searchState.showSearchDrawer.set(false)} /> |  | ||||||
|         </div> |  | ||||||
|       </div> |  | ||||||
|     <SearchResults {state} /> |     <SearchResults {state} /> | ||||||
|     </div> |  | ||||||
|   </DrawerRight> |   </DrawerRight> | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|   <!-- Top components --> |   <!-- Top components --> | ||||||
|   <div class="pointer-events-none absolute top-0 left-0 w-full"> |   <div class="pointer-events-none absolute top-0 left-0 w-full"> | ||||||
| 
 | 
 | ||||||
|  | @ -356,9 +357,22 @@ | ||||||
|       {/if} |       {/if} | ||||||
| 
 | 
 | ||||||
|       <If condition={state.featureSwitches.featureSwitchSearch}> |       <If condition={state.featureSwitches.featureSwitchSearch}> | ||||||
|  |         <div class="flex items-center"> | ||||||
|           <div class="w-full sm:w-64"> |           <div class="w-full sm:w-64"> | ||||||
|             <Searchbar value={state.searchState.searchTerm} isFocused={state.searchState.searchIsFocused} /> |             <Searchbar value={state.searchState.searchTerm} isFocused={state.searchState.searchIsFocused} /> | ||||||
|           </div> |           </div> | ||||||
|  |           <MapControlButton on:keydown={forwardEventToMap} on:click={() =>{ | ||||||
|  |             if(searchOpened.data){ | ||||||
|  |               searchOpened.set(false) | ||||||
|  |             }else{ | ||||||
|  |               state.searchState.searchIsFocused.set(true) | ||||||
|  |             } | ||||||
|  |             }}> | ||||||
|  |             <ChevronRight class="w-7 h-7 p-0 m-0 transition-all" | ||||||
|  |                           style={"rotate: " + ($searchOpened ?  "0deg" : "180deg" ) } /> | ||||||
|  |           </MapControlButton> | ||||||
|  |         </div> | ||||||
|  | 
 | ||||||
|       </If> |       </If> | ||||||
| 
 | 
 | ||||||
|     </div> |     </div> | ||||||
|  |  | ||||||
							
								
								
									
										11
									
								
								src/Utils.ts
									
										
									
									
									
								
							
							
						
						
									
										11
									
								
								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]) |         return withDistance.map((n) => n[0]) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | 
 | ||||||
|     public static levenshteinDistance(str1: string, str2: string): number { |     public static levenshteinDistance(str1: string, str2: string): number { | ||||||
|         const track: number[][] = Array(str2.length + 1) |         const track: number[][] = Array(str2.length + 1) | ||||||
|             .fill(null) |             .fill(null) | ||||||
|  | @ -1437,6 +1438,14 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be | ||||||
|         return d |         return d | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     public static asRecord<K extends string | number | symbol, V>(keys: K[], f: ((k: K) => V)): Record<K, V> { | ||||||
|  |         const results = <Record<K, V>> {} | ||||||
|  |         for (const key of keys) { | ||||||
|  |             results[key] = f(key) | ||||||
|  |         } | ||||||
|  |         return results | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     static toIdRecord<T extends { id: string }>(ts: T[]): Record<string, T> { |     static toIdRecord<T extends { id: string }>(ts: T[]): Record<string, T> { | ||||||
|         const result: Record<string, T> = {} |         const result: Record<string, T> = {} | ||||||
|         for (const t of ts) { |         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<T>(items: T[]): T[] { |     public static NoNullInplace<T>(items: T[]): T[] { | ||||||
|         for (let i = items.length - 1; i >= 0; i--) { |         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) |                 items.splice(i, 1) | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue