forked from MapComplete/MapComplete
		
	Search feature: refactor, add translations
This commit is contained in:
		
							parent
							
								
									b3492930b8
								
							
						
					
					
						commit
						bd3bddc89c
					
				
					 21 changed files with 499 additions and 507 deletions
				
			
		| 
						 | 
				
			
			@ -397,11 +397,21 @@
 | 
			
		|||
        "save": "Save",
 | 
			
		||||
        "screenToSmall": "Open <i>{theme}</i> in a new window",
 | 
			
		||||
        "search": {
 | 
			
		||||
            "activeFilters": "Active filters",
 | 
			
		||||
            "clearFilters": "Clear filters",
 | 
			
		||||
            "deleteSearchHistory": "Delete location history",
 | 
			
		||||
            "deleteThemeHistory": "Delete earlier visited themes",
 | 
			
		||||
            "editSearchSyncSettings": "Edit sync settings",
 | 
			
		||||
            "editThemeSync": "Edit sync settings",
 | 
			
		||||
            "error": "Something went wrong…",
 | 
			
		||||
            "instructions": "Use the search bar above to search for locations, filters or other thematic maps",
 | 
			
		||||
            "locations": "Locations",
 | 
			
		||||
            "nothing": "Nothing found…",
 | 
			
		||||
            "nothingFor": "No results found for {term}",
 | 
			
		||||
            "otherMaps": "Other maps",
 | 
			
		||||
            "pickFilter": "Pick a filter",
 | 
			
		||||
            "recentThemes": "Recently visited maps",
 | 
			
		||||
            "recents": "Recent searches",
 | 
			
		||||
            "recents": "Recently seen places",
 | 
			
		||||
            "search": "Search a location",
 | 
			
		||||
            "searchShort": "Search…",
 | 
			
		||||
            "searching": "Searching…"
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -333,9 +333,21 @@
 | 
			
		|||
        "save": "Opslaan",
 | 
			
		||||
        "screenToSmall": "Open {theme} in een nieuw venster",
 | 
			
		||||
        "search": {
 | 
			
		||||
            "activeFilters": "Actieve filters",
 | 
			
		||||
            "clearFilters": "Verwijder filters",
 | 
			
		||||
            "deleteSearchHistory": "Verwijder geschiedenis",
 | 
			
		||||
            "deleteThemeHistory": "Verwijder geschiedenis",
 | 
			
		||||
            "editSearchSyncSettings": "Stel je geschiedenis-voorkeuren in",
 | 
			
		||||
            "editThemeSync": "Stel je geschiedenis-voorkeuren in",
 | 
			
		||||
            "error": "Niet gelukt…",
 | 
			
		||||
            "instructions": "Gebruik de zoekbalk om locaties, filters of om andere kaarten te zoeken",
 | 
			
		||||
            "locations": "Plaatsen",
 | 
			
		||||
            "nothing": "Niets gevonden…",
 | 
			
		||||
            "search": "Zoek naar een locatie",
 | 
			
		||||
            "otherMaps": "Andere kaarten",
 | 
			
		||||
            "pickFilter": "Kies een filter",
 | 
			
		||||
            "recentThemes": "Recent bezochte kaarten",
 | 
			
		||||
            "recents": "Recent bekeken plaatsen",
 | 
			
		||||
            "search": "Zoek naar een locatie, filter of kaart",
 | 
			
		||||
            "searchShort": "Zoek…",
 | 
			
		||||
            "searching": "Aan het zoeken…"
 | 
			
		||||
        },
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2,11 +2,11 @@ import GeocodingProvider, { SearchResult, GeocodingOptions, GeocodeResult } from
 | 
			
		|||
import { Utils } from "../../Utils"
 | 
			
		||||
import { Store, Stores } from "../UIEventSource"
 | 
			
		||||
 | 
			
		||||
export default class CombinedSearcher implements GeocodingProvider <GeocodeResult> {
 | 
			
		||||
    private _providers: ReadonlyArray<GeocodingProvider<GeocodeResult>>
 | 
			
		||||
    private _providersWithSuggest: ReadonlyArray<GeocodingProvider<GeocodeResult>>
 | 
			
		||||
export default class CombinedSearcher implements GeocodingProvider {
 | 
			
		||||
    private _providers: ReadonlyArray<GeocodingProvider>
 | 
			
		||||
    private _providersWithSuggest: ReadonlyArray<GeocodingProvider>
 | 
			
		||||
 | 
			
		||||
    constructor(...providers: ReadonlyArray<GeocodingProvider<GeocodeResult>>) {
 | 
			
		||||
    constructor(...providers: ReadonlyArray<GeocodingProvider>) {
 | 
			
		||||
        this._providers = Utils.NoNull(providers)
 | 
			
		||||
        this._providersWithSuggest = this._providers.filter(pr => pr.suggest !== undefined)
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -5,7 +5,7 @@ import { ImmutableStore, Store } from "../UIEventSource"
 | 
			
		|||
/**
 | 
			
		||||
 * A simple search-class which interprets possible locations
 | 
			
		||||
 */
 | 
			
		||||
export default class CoordinateSearch implements GeocodingProvider<GeocodeResult> {
 | 
			
		||||
export default class CoordinateSearch implements GeocodingProvider {
 | 
			
		||||
    private static readonly latLonRegexes: ReadonlyArray<RegExp> = [
 | 
			
		||||
        /^(-?[0-9]+\.[0-9]+)[ ,;/\\]+(-?[0-9]+\.[0-9]+)/,
 | 
			
		||||
        /lat[:=]? *['"]?(-?[0-9]+\.[0-9]+)['"]?[ ,;&]+lon[:=]? *['"]?(-?[0-9]+\.[0-9]+)['"]?/,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,24 +1,24 @@
 | 
			
		|||
import { ImmutableStore, Store } from "../UIEventSource"
 | 
			
		||||
import GeocodingProvider, { FilterPayload, FilterResult, GeocodingOptions, SearchResult } from "./GeocodingProvider"
 | 
			
		||||
import { SpecialVisualizationState } from "../../UI/SpecialVisualization"
 | 
			
		||||
import { Utils } from "../../Utils"
 | 
			
		||||
import Locale from "../../UI/i18n/Locale"
 | 
			
		||||
import Constants from "../../Models/Constants"
 | 
			
		||||
import FilterConfig, { FilterConfigOption } from "../../Models/ThemeConfig/FilterConfig"
 | 
			
		||||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
 | 
			
		||||
 | 
			
		||||
export type FilterSearchResult = { option: FilterConfigOption, filter: FilterConfig, layer: LayerConfig, index: number }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Searches matching filters
 | 
			
		||||
 */
 | 
			
		||||
export default class FilterSearch implements GeocodingProvider {
 | 
			
		||||
export default class FilterSearch {
 | 
			
		||||
    private readonly _state: SpecialVisualizationState
 | 
			
		||||
 | 
			
		||||
    constructor(state: SpecialVisualizationState) {
 | 
			
		||||
        this._state = state
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async search(query: string): Promise<SearchResult[]> {
 | 
			
		||||
        return this.searchDirectly(query)
 | 
			
		||||
    }
 | 
			
		||||
    public searchDirectly(query: string): FilterResult[] {
 | 
			
		||||
    public search(query: string): FilterSearchResult[] {
 | 
			
		||||
        if (query.length === 0) {
 | 
			
		||||
            return []
 | 
			
		||||
        }
 | 
			
		||||
| 
						 | 
				
			
			@ -28,7 +28,7 @@ export default class FilterSearch implements GeocodingProvider {
 | 
			
		|||
            }
 | 
			
		||||
            return query
 | 
			
		||||
        }).filter(q => q.length > 0)
 | 
			
		||||
        const possibleFilters: FilterResult[] = []
 | 
			
		||||
        const possibleFilters: FilterSearchResult[] = []
 | 
			
		||||
        for (const layer of this._state.layout.layers) {
 | 
			
		||||
            if (!Array.isArray(layer.filters)) {
 | 
			
		||||
                continue
 | 
			
		||||
| 
						 | 
				
			
			@ -61,13 +61,9 @@ export default class FilterSearch implements GeocodingProvider {
 | 
			
		|||
                    if (levehnsteinD > 0.25) {
 | 
			
		||||
                        continue
 | 
			
		||||
                    }
 | 
			
		||||
                    possibleFilters.push(<FilterResult>{
 | 
			
		||||
                        category: "filter",
 | 
			
		||||
                        osm_id: layer.id + "/" + filter.id + "/" + i,
 | 
			
		||||
                        payload: {
 | 
			
		||||
                    possibleFilters.push({
 | 
			
		||||
                            option, layer, filter, index:
 | 
			
		||||
                            i,
 | 
			
		||||
                        },
 | 
			
		||||
                    })
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
| 
						 | 
				
			
			@ -75,16 +71,11 @@ export default class FilterSearch implements GeocodingProvider {
 | 
			
		|||
        return possibleFilters
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    suggest(query: string, options?: GeocodingOptions): Store<SearchResult[]> {
 | 
			
		||||
        return new ImmutableStore(this.searchDirectly(query))
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Create a random list of filters
 | 
			
		||||
     */
 | 
			
		||||
    getSuggestions(): FilterPayload[] {
 | 
			
		||||
        const result: FilterPayload[] = []
 | 
			
		||||
    getSuggestions(): FilterSearchResult[] {
 | 
			
		||||
        const result: FilterSearchResult[] = []
 | 
			
		||||
        for (const [id, filteredLayer] of this._state.layerState.filteredLayers) {
 | 
			
		||||
            if (!Array.isArray(filteredLayer.layerDef.filters)) {
 | 
			
		||||
                continue
 | 
			
		||||
| 
						 | 
				
			
			@ -93,7 +84,7 @@ export default class FilterSearch implements GeocodingProvider {
 | 
			
		|||
                continue
 | 
			
		||||
            }
 | 
			
		||||
            for (const filter of filteredLayer.layerDef.filters) {
 | 
			
		||||
                const singleFilterResults: FilterPayload[] = []
 | 
			
		||||
                const singleFilterResults: FilterSearchResult[] = []
 | 
			
		||||
                for (let i = 0; i < Math.min(filter.options.length, 5); i++) {
 | 
			
		||||
                    const option = filter.options[i]
 | 
			
		||||
                    if (option.osmTags === undefined) {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -5,8 +5,6 @@ import { Store } from "../UIEventSource"
 | 
			
		|||
import * as search from "../../assets/generated/layers/search.json"
 | 
			
		||||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
 | 
			
		||||
import { LayerConfigJson } from "../../Models/ThemeConfig/Json/LayerConfigJson"
 | 
			
		||||
import FilterConfig, { FilterConfigOption } from "../../Models/ThemeConfig/FilterConfig"
 | 
			
		||||
import { MinimalLayoutInformation } from "../../Models/ThemeConfig/LayoutConfig"
 | 
			
		||||
import { GeoOperations } from "../GeoOperations"
 | 
			
		||||
 | 
			
		||||
export type GeocodingCategory =
 | 
			
		||||
| 
						 | 
				
			
			@ -44,13 +42,7 @@ export type GeocodeResult =  {
 | 
			
		|||
    payload?: object,
 | 
			
		||||
    source?: string
 | 
			
		||||
}
 | 
			
		||||
export type FilterPayload = { option: FilterConfigOption, filter: FilterConfig, layer: LayerConfig, index: number }
 | 
			
		||||
export type FilterResult =  { category: "filter", osm_id: string, payload:  FilterPayload }
 | 
			
		||||
export type LayerResult = {category: "layer", osm_id: string, payload: LayerConfig}
 | 
			
		||||
export type SearchResult =
 | 
			
		||||
    | FilterResult
 | 
			
		||||
    | { category: "theme", osm_id: string, payload: MinimalLayoutInformation }
 | 
			
		||||
    | LayerResult
 | 
			
		||||
    | GeocodeResult
 | 
			
		||||
 | 
			
		||||
export interface GeocodingOptions {
 | 
			
		||||
| 
						 | 
				
			
			@ -58,16 +50,16 @@ export interface GeocodingOptions {
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
export default interface GeocodingProvider<T extends SearchResult = SearchResult> {
 | 
			
		||||
export default interface GeocodingProvider {
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    search(query: string, options?: GeocodingOptions): Promise<T[]>
 | 
			
		||||
    search(query: string, options?: GeocodingOptions): Promise<GeocodeResult[]>
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @param query
 | 
			
		||||
     * @param options
 | 
			
		||||
     */
 | 
			
		||||
    suggest?(query: string, options?: GeocodingOptions): Store<T[]>
 | 
			
		||||
    suggest?(query: string, options?: GeocodingOptions): Store<GeocodeResult[]>
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export type ReverseGeocodingResult = Feature<Geometry, {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,44 +1,41 @@
 | 
			
		|||
import GeocodingProvider, { GeocodingOptions, LayerResult, SearchResult } from "./GeocodingProvider"
 | 
			
		||||
import { SpecialVisualizationState } from "../../UI/SpecialVisualization"
 | 
			
		||||
import MoreScreen from "../../UI/BigComponents/MoreScreen"
 | 
			
		||||
import { ImmutableStore, Store } from "../UIEventSource"
 | 
			
		||||
import Constants from "../../Models/Constants"
 | 
			
		||||
import SearchUtils from "./SearchUtils"
 | 
			
		||||
import ThemeSearch from "./ThemeSearch"
 | 
			
		||||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
 | 
			
		||||
 | 
			
		||||
export default class LayerSearch implements GeocodingProvider<LayerResult> {
 | 
			
		||||
export default class LayerSearch {
 | 
			
		||||
 | 
			
		||||
    private readonly _state: SpecialVisualizationState
 | 
			
		||||
    private readonly _suggestionLimit: number
 | 
			
		||||
    private readonly _layerWhitelist : Set<string>
 | 
			
		||||
    constructor(state: SpecialVisualizationState, suggestionLimit: number) {
 | 
			
		||||
    constructor(state: SpecialVisualizationState) {
 | 
			
		||||
        this._state = state
 | 
			
		||||
        this._layerWhitelist = new Set(state.layout.layers.map(l => l.id).filter(id => Constants.added_by_default.indexOf(<any> id) < 0))
 | 
			
		||||
        this._suggestionLimit = suggestionLimit
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async search(query: string): Promise<LayerResult[]> {
 | 
			
		||||
        return this.searchWrapped(query, 99)
 | 
			
		||||
    static scoreLayers(query: string, layerWhitelist?: Set<string>): Record<string, number> {
 | 
			
		||||
        const result: Record<string, number> = {}
 | 
			
		||||
        for (const id in ThemeSearch.officialThemes.layers) {
 | 
			
		||||
            if(layerWhitelist !== undefined && !layerWhitelist.has(id)){
 | 
			
		||||
                continue
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
    suggest(query: string, options?: GeocodingOptions): Store<LayerResult[]> {
 | 
			
		||||
        return new ImmutableStore(this.searchWrapped(query, this._suggestionLimit ?? 4))
 | 
			
		||||
            const keywords = ThemeSearch.officialThemes.layers[id]
 | 
			
		||||
            const distance = SearchUtils.scoreKeywords(query, keywords)
 | 
			
		||||
            result[id] = distance
 | 
			
		||||
        }
 | 
			
		||||
        return result
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    private searchWrapped(query: string, limit: number): LayerResult[] {
 | 
			
		||||
        return this.searchDirect(query, limit)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public searchDirect(query: string, limit: number): LayerResult[] {
 | 
			
		||||
    public search(query: string, limit: number): LayerConfig[] {
 | 
			
		||||
        if (query.length < 1) {
 | 
			
		||||
            return []
 | 
			
		||||
        }
 | 
			
		||||
        const scores = MoreScreen.scoreLayers(query, this._layerWhitelist)
 | 
			
		||||
        const asList:(LayerResult & {score:number})[] = []
 | 
			
		||||
        const scores = LayerSearch.scoreLayers(query, this._layerWhitelist)
 | 
			
		||||
        const asList:({layer: LayerConfig, score:number})[] = []
 | 
			
		||||
        for (const layer in scores) {
 | 
			
		||||
            asList.push({
 | 
			
		||||
                category: "layer",
 | 
			
		||||
                payload: this._state.layout.getLayer(layer),
 | 
			
		||||
                osm_id: layer,
 | 
			
		||||
                layer: this._state.layout.getLayer(layer),
 | 
			
		||||
                score: scores[layer]
 | 
			
		||||
            })
 | 
			
		||||
        }
 | 
			
		||||
| 
						 | 
				
			
			@ -47,6 +44,7 @@ export default class LayerSearch implements GeocodingProvider<LayerResult> {
 | 
			
		|||
        return asList
 | 
			
		||||
            .filter(sorted => sorted.score < 2)
 | 
			
		||||
            .slice(0, limit)
 | 
			
		||||
            .map(l => l.layer)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -3,7 +3,7 @@ import GeocodingProvider, { GeocodingOptions, GeocodeResult } from "./GeocodingP
 | 
			
		|||
import { OsmId } from "../../Models/OsmFeature"
 | 
			
		||||
import { SpecialVisualizationState } from "../../UI/SpecialVisualization"
 | 
			
		||||
 | 
			
		||||
export default class OpenStreetMapIdSearch implements GeocodingProvider<GeocodeResult> {
 | 
			
		||||
export default class OpenStreetMapIdSearch implements GeocodingProvider {
 | 
			
		||||
    private static readonly regex = /((https?:\/\/)?(www.)?(osm|openstreetmap).org\/)?(n|node|w|way|r|relation)[/ ]?([0-9]+)/
 | 
			
		||||
 | 
			
		||||
    private static readonly types: Readonly<Record<string, "node" | "way" | "relation">> = {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										75
									
								
								src/Logic/Search/SearchUtils.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										75
									
								
								src/Logic/Search/SearchUtils.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,75 @@
 | 
			
		|||
import Locale from "../../UI/i18n/Locale"
 | 
			
		||||
import { Utils } from "../../Utils"
 | 
			
		||||
import ThemeSearch from "./ThemeSearch"
 | 
			
		||||
 | 
			
		||||
export default class SearchUtils {
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    /** 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 applySpecialSearch(searchTerm: string, ) {
 | 
			
		||||
        searchTerm = searchTerm.toLowerCase()
 | 
			
		||||
        if (!searchTerm) {
 | 
			
		||||
            return false
 | 
			
		||||
        }
 | 
			
		||||
        if (searchTerm === "personal") {
 | 
			
		||||
            window.location.href = ThemeSearch.createUrlFor({ id: "personal" }, undefined)
 | 
			
		||||
        }
 | 
			
		||||
        if (searchTerm === "bugs" || searchTerm === "issues") {
 | 
			
		||||
            window.location.href = "https://github.com/pietervdvn/MapComplete/issues"
 | 
			
		||||
        }
 | 
			
		||||
        if (searchTerm === "source") {
 | 
			
		||||
            window.location.href = "https://github.com/pietervdvn/MapComplete"
 | 
			
		||||
        }
 | 
			
		||||
        if (searchTerm === "docs") {
 | 
			
		||||
            window.location.href = "https://github.com/pietervdvn/MapComplete/tree/develop/Docs"
 | 
			
		||||
        }
 | 
			
		||||
        if (searchTerm === "osmcha" || searchTerm === "stats") {
 | 
			
		||||
            window.location.href = Utils.OsmChaLinkFor(7)
 | 
			
		||||
        }
 | 
			
		||||
        if (searchTerm === "studio") {
 | 
			
		||||
            window.location.href = "./studio.html"
 | 
			
		||||
        }
 | 
			
		||||
        return false
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Searches for the smallest distance in words; will split both the query and the terms
 | 
			
		||||
     *
 | 
			
		||||
     * SearchUtils.scoreKeywords("drinking water", {"en": ["A layer with drinking water points"]}, "en") // => 0
 | 
			
		||||
     * SearchUtils.scoreKeywords("waste", {"en": ["A layer with drinking water points"]}, "en") // => 2
 | 
			
		||||
     *
 | 
			
		||||
     */
 | 
			
		||||
    public static scoreKeywords(query: string, keywords: Record<string, string[]> | string[], language?: string): number {
 | 
			
		||||
        if(!keywords){
 | 
			
		||||
            return Infinity
 | 
			
		||||
        }
 | 
			
		||||
        language ??= Locale.language.data
 | 
			
		||||
        const queryParts = query.trim().split(" ").map(q => Utils.simplifyStringForSearch(q))
 | 
			
		||||
        let terms: string[]
 | 
			
		||||
        if (Array.isArray(keywords)) {
 | 
			
		||||
            terms = keywords
 | 
			
		||||
        } else {
 | 
			
		||||
            terms = (keywords[language] ?? []).concat(keywords["*"])
 | 
			
		||||
        }
 | 
			
		||||
        const termsAll = Utils.NoNullInplace(terms).flatMap(t => t.split(" "))
 | 
			
		||||
 | 
			
		||||
        let distanceSummed = 0
 | 
			
		||||
        for (let i = 0; i < queryParts.length; i++) {
 | 
			
		||||
            const q = queryParts[i]
 | 
			
		||||
            let minDistance: number = 99
 | 
			
		||||
            for (const term of termsAll) {
 | 
			
		||||
                const d = Utils.levenshteinDistance(q, Utils.simplifyStringForSearch(term))
 | 
			
		||||
                if (d < minDistance) {
 | 
			
		||||
                    minDistance = d
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            distanceSummed += minDistance
 | 
			
		||||
        }
 | 
			
		||||
        return distanceSummed
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,49 +1,55 @@
 | 
			
		|||
import GeocodingProvider, { GeocodingOptions, SearchResult } from "./GeocodingProvider"
 | 
			
		||||
import { MinimalLayoutInformation } from "../../Models/ThemeConfig/LayoutConfig"
 | 
			
		||||
import { SpecialVisualizationState } from "../../UI/SpecialVisualization"
 | 
			
		||||
import MoreScreen from "../../UI/BigComponents/MoreScreen"
 | 
			
		||||
import { ImmutableStore, Store } from "../UIEventSource"
 | 
			
		||||
import { Store } from "../UIEventSource"
 | 
			
		||||
import UserRelatedState from "../State/UserRelatedState"
 | 
			
		||||
import { Utils } from "../../Utils"
 | 
			
		||||
import Locale from "../../UI/i18n/Locale"
 | 
			
		||||
import themeOverview from "../../assets/generated/theme_overview.json"
 | 
			
		||||
import LayerSearch from "./LayerSearch"
 | 
			
		||||
import SearchUtils from "./SearchUtils"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
type ThemeSearchScore = {
 | 
			
		||||
    theme: MinimalLayoutInformation,
 | 
			
		||||
    lowest: number,
 | 
			
		||||
    perLayer?: Record<string, number>,
 | 
			
		||||
    other: number
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
export default class ThemeSearch {
 | 
			
		||||
 | 
			
		||||
    public static readonly officialThemes: {
 | 
			
		||||
        themes: MinimalLayoutInformation[],
 | 
			
		||||
        layers: Record<string, Record<string, string[]>>
 | 
			
		||||
    } = themeOverview
 | 
			
		||||
    public static readonly officialThemesById: Map<string, MinimalLayoutInformation> = new Map<string, MinimalLayoutInformation>()
 | 
			
		||||
    static {
 | 
			
		||||
        for (const th of ThemeSearch.officialThemes.themes ?? []) {
 | 
			
		||||
            ThemeSearch.officialThemesById.set(th.id, th)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
export default class ThemeSearch implements GeocodingProvider {
 | 
			
		||||
 | 
			
		||||
    private readonly _state: SpecialVisualizationState
 | 
			
		||||
    private readonly _knownHiddenThemes: Store<Set<string>>
 | 
			
		||||
    private readonly _suggestionLimit: number
 | 
			
		||||
    private readonly _layersToIgnore: string[]
 | 
			
		||||
    private readonly _otherThemes: MinimalLayoutInformation[]
 | 
			
		||||
 | 
			
		||||
    constructor(state: SpecialVisualizationState, suggestionLimit: number) {
 | 
			
		||||
    constructor(state: SpecialVisualizationState) {
 | 
			
		||||
        this._state = state
 | 
			
		||||
        this._layersToIgnore = state.layout.layers.map(l => l.id)
 | 
			
		||||
        this._suggestionLimit = suggestionLimit
 | 
			
		||||
        this._knownHiddenThemes = UserRelatedState.initDiscoveredHiddenThemes(this._state.osmConnection).map(list => new Set(list))
 | 
			
		||||
        this._otherThemes = MoreScreen.officialThemes.themes
 | 
			
		||||
        this._otherThemes = ThemeSearch.officialThemes.themes
 | 
			
		||||
            .filter(th => th.id !== state.layout.id)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async search(query: string): Promise<SearchResult[]> {
 | 
			
		||||
        return this.searchWrapped(query, 99)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    suggest(query: string, options?: GeocodingOptions): Store<SearchResult[]> {
 | 
			
		||||
        return new ImmutableStore(this.searchWrapped(query, this._suggestionLimit ?? 4))
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    private searchWrapped(query: string, limit: number): SearchResult[] {
 | 
			
		||||
        return this.searchDirect(query, limit).map(match => <SearchResult>{
 | 
			
		||||
            payload: match,
 | 
			
		||||
            category: "theme",
 | 
			
		||||
            osm_id: match.id
 | 
			
		||||
        })
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public searchDirect(query: string, limit: number): MinimalLayoutInformation[] {
 | 
			
		||||
    public search(query: string, limit: number): MinimalLayoutInformation[] {
 | 
			
		||||
        if (query.length < 1) {
 | 
			
		||||
            return []
 | 
			
		||||
        }
 | 
			
		||||
        const sorted = MoreScreen.sortedByLowest(query, this._otherThemes, this._layersToIgnore)
 | 
			
		||||
        const sorted = ThemeSearch.sortedByLowestScores(query, this._otherThemes, this._layersToIgnore)
 | 
			
		||||
        return sorted
 | 
			
		||||
            .filter(sorted => sorted.lowest < 2)
 | 
			
		||||
            .map(th => th.theme)
 | 
			
		||||
| 
						 | 
				
			
			@ -51,5 +57,96 @@ export default class ThemeSearch implements GeocodingProvider {
 | 
			
		|||
            .slice(0, limit)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static createUrlFor(
 | 
			
		||||
        layout: { id: string },
 | 
			
		||||
        state?: { layoutToUse?: { id } },
 | 
			
		||||
    ): string {
 | 
			
		||||
        if (layout === undefined) {
 | 
			
		||||
            return undefined
 | 
			
		||||
        }
 | 
			
		||||
        if (layout.id === undefined) {
 | 
			
		||||
            console.error("ID is undefined for layout", layout)
 | 
			
		||||
            return undefined
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (layout.id === state?.layoutToUse?.id) {
 | 
			
		||||
            return undefined
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        let path = window.location.pathname
 | 
			
		||||
        // Path starts with a '/' and contains everything, e.g. '/dir/dir/page.html'
 | 
			
		||||
        path = path.substr(0, path.lastIndexOf("/"))
 | 
			
		||||
        // Path will now contain '/dir/dir', or empty string in case of nothing
 | 
			
		||||
        if (path === "") {
 | 
			
		||||
            path = "."
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        let linkPrefix = `${path}/${layout.id.toLowerCase()}.html?`
 | 
			
		||||
        if (location.hostname === "localhost" || location.hostname === "127.0.0.1") {
 | 
			
		||||
            linkPrefix = `${path}/theme.html?layout=${layout.id}&`
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (layout.id.startsWith("http://") || layout.id.startsWith("https://")) {
 | 
			
		||||
            linkPrefix = `${path}/theme.html?userlayout=${layout.id}&`
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        return `${linkPrefix}`
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static scoreThemes(query: string, themes: MinimalLayoutInformation[], ignoreLayers: string[] = []): Record<string, ThemeSearchScore> {
 | 
			
		||||
        if (query?.length < 1) {
 | 
			
		||||
            return undefined
 | 
			
		||||
        }
 | 
			
		||||
        themes = Utils.NoNullInplace(themes)
 | 
			
		||||
        const layerScores = LayerSearch.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
 | 
			
		||||
            }
 | 
			
		||||
            if (Utils.simplifyStringForSearch(theme) === query) {
 | 
			
		||||
                results[theme] = {
 | 
			
		||||
                    theme: layoutInfo,
 | 
			
		||||
                    lowest: -1,
 | 
			
		||||
                    other: 0,
 | 
			
		||||
                }
 | 
			
		||||
                continue
 | 
			
		||||
            }
 | 
			
		||||
            const perLayer = Utils.asRecord(
 | 
			
		||||
                layoutInfo.layers ?? [], layer => layerScores[layer],
 | 
			
		||||
            )
 | 
			
		||||
            const language = Locale.language.data
 | 
			
		||||
 | 
			
		||||
            const keywords = Utils.NoNullInplace([layoutInfo.shortDescription, layoutInfo.title])
 | 
			
		||||
                .map(item => typeof item === "string" ? item : (item[language] ?? item["*"]))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
            const other = Math.min(SearchUtils.scoreKeywords(query, keywords), SearchUtils.scoreKeywords(query, layoutInfo.keywords))
 | 
			
		||||
            const lowest = Math.min(other, ...Object.values(perLayer))
 | 
			
		||||
            results[theme] = {
 | 
			
		||||
                theme: layoutInfo,
 | 
			
		||||
                perLayer,
 | 
			
		||||
                other,
 | 
			
		||||
                lowest,
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        return results
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static sortedByLowestScores(search: string, themes: MinimalLayoutInformation[], ignoreLayers: string[] = []): ThemeSearchScore[] {
 | 
			
		||||
        const scored = Object.values(this.scoreThemes(search, themes, ignoreLayers))
 | 
			
		||||
        scored.sort((a, b) => a.lowest - b.lowest)
 | 
			
		||||
        return scored
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static sortedByLowest(search: string, themes: MinimalLayoutInformation[], ignoreLayers: string[] = [], maxDiff: number): MinimalLayoutInformation[] {
 | 
			
		||||
        return this.sortedByLowestScores(search, themes, ignoreLayers)
 | 
			
		||||
            .map(th => th.theme)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,38 +1,30 @@
 | 
			
		|||
import GeocodingProvider, {
 | 
			
		||||
    FilterPayload, FilterResult,
 | 
			
		||||
    GeocodeResult,
 | 
			
		||||
    GeocodingUtils, LayerResult,
 | 
			
		||||
    type SearchResult,
 | 
			
		||||
} from "../Search/GeocodingProvider"
 | 
			
		||||
import GeocodingProvider, { GeocodingUtils, type SearchResult } from "../Search/GeocodingProvider"
 | 
			
		||||
import { ImmutableStore, Store, Stores, UIEventSource } from "../UIEventSource"
 | 
			
		||||
import CombinedSearcher from "../Search/CombinedSearcher"
 | 
			
		||||
import FilterSearch from "../Search/FilterSearch"
 | 
			
		||||
import FilterSearch, { FilterSearchResult } from "../Search/FilterSearch"
 | 
			
		||||
import LocalElementSearch from "../Search/LocalElementSearch"
 | 
			
		||||
import CoordinateSearch from "../Search/CoordinateSearch"
 | 
			
		||||
import ThemeSearch from "../Search/ThemeSearch"
 | 
			
		||||
import OpenStreetMapIdSearch from "../Search/OpenStreetMapIdSearch"
 | 
			
		||||
import PhotonSearch from "../Search/PhotonSearch"
 | 
			
		||||
import ThemeViewState from "../../Models/ThemeViewState"
 | 
			
		||||
import Translations from "../../UI/i18n/Translations"
 | 
			
		||||
import type { MinimalLayoutInformation } from "../../Models/ThemeConfig/LayoutConfig"
 | 
			
		||||
import MoreScreen from "../../UI/BigComponents/MoreScreen"
 | 
			
		||||
import { BBox } from "../BBox"
 | 
			
		||||
import { Translation } from "../../UI/i18n/Translation"
 | 
			
		||||
import GeocodingFeatureSource from "../Search/GeocodingFeatureSource"
 | 
			
		||||
import ShowDataLayer from "../../UI/Map/ShowDataLayer"
 | 
			
		||||
import LayerSearch from "../Search/LayerSearch"
 | 
			
		||||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
 | 
			
		||||
 | 
			
		||||
export default class SearchState {
 | 
			
		||||
 | 
			
		||||
    public readonly isSearching = new UIEventSource(false)
 | 
			
		||||
    public readonly feedback: UIEventSource<Translation> = new UIEventSource<Translation>(undefined)
 | 
			
		||||
    public readonly searchTerm: UIEventSource<string> = new UIEventSource<string>("")
 | 
			
		||||
    public readonly searchIsFocused = new UIEventSource(false)
 | 
			
		||||
    public readonly suggestions: Store<SearchResult[]>
 | 
			
		||||
    public readonly filterSuggestions: Store<FilterResult[]>
 | 
			
		||||
    public readonly filterSuggestions: Store<FilterSearchResult[]>
 | 
			
		||||
    public readonly themeSuggestions: Store<MinimalLayoutInformation[]>
 | 
			
		||||
    public readonly layerSuggestions: Store<LayerResult[]>
 | 
			
		||||
    public readonly locationSearchers: ReadonlyArray<GeocodingProvider<GeocodeResult>>
 | 
			
		||||
    public readonly layerSuggestions: Store<LayerConfig[]>
 | 
			
		||||
    public readonly locationSearchers: ReadonlyArray<GeocodingProvider>
 | 
			
		||||
 | 
			
		||||
    private readonly state: ThemeViewState
 | 
			
		||||
    public readonly showSearchDrawer: UIEventSource<boolean>
 | 
			
		||||
| 
						 | 
				
			
			@ -42,7 +34,7 @@ export default class SearchState {
 | 
			
		|||
        this.state = state
 | 
			
		||||
 | 
			
		||||
        this.locationSearchers = [
 | 
			
		||||
            // new LocalElementSearch(state, 5),
 | 
			
		||||
            new LocalElementSearch(state, 5),
 | 
			
		||||
            new CoordinateSearch(),
 | 
			
		||||
            new OpenStreetMapIdSearch(state),
 | 
			
		||||
            new PhotonSearch(), // new NominatimGeocoding(),
 | 
			
		||||
| 
						 | 
				
			
			@ -67,18 +59,18 @@ export default class SearchState {
 | 
			
		|||
            Stores.concat(suggestions).map(suggestions => CombinedSearcher.merge(suggestions)),
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        const themeSearch = new ThemeSearch(state, 3)
 | 
			
		||||
        this.themeSuggestions = this.searchTerm.mapD(query => themeSearch.searchDirect(query, 3))
 | 
			
		||||
        const themeSearch = new ThemeSearch(state)
 | 
			
		||||
        this.themeSuggestions = this.searchTerm.mapD(query => themeSearch.search(query, 3))
 | 
			
		||||
 | 
			
		||||
        const layerSearch = new LayerSearch(state, 5)
 | 
			
		||||
        this.layerSuggestions = this.searchTerm.mapD(query => layerSearch.searchDirect(query, 5))
 | 
			
		||||
        const layerSearch = new LayerSearch(state)
 | 
			
		||||
        this.layerSuggestions = this.searchTerm.mapD(query => layerSearch.search(query, 5))
 | 
			
		||||
 | 
			
		||||
        const filterSearch = new FilterSearch(state)
 | 
			
		||||
        this.filterSuggestions = this.searchTerm.stabilized(50)
 | 
			
		||||
            .mapD(query => filterSearch.searchDirectly(query))
 | 
			
		||||
            .mapD(query => filterSearch.search(query))
 | 
			
		||||
            .mapD(filterResult => {
 | 
			
		||||
                const active = state.layerState.activeFilters.data
 | 
			
		||||
                return filterResult.filter(({ payload: { filter, index, layer } }) => {
 | 
			
		||||
                return filterResult.filter(({ filter, index, layer }) => {
 | 
			
		||||
                    const foundMatch = active.some(active =>
 | 
			
		||||
                        active.filter.id === filter.id && layer.id === active.layer.id && active.control.data === index)
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -108,22 +100,20 @@ export default class SearchState {
 | 
			
		|||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async apply(result: FilterResult | LayerResult) {
 | 
			
		||||
        if (result.category === "filter") {
 | 
			
		||||
            return this.applyFilter(result.payload)
 | 
			
		||||
        }
 | 
			
		||||
        if (result.category === "layer") {
 | 
			
		||||
    public async apply(result: FilterSearchResult | LayerConfig) {
 | 
			
		||||
        if (result instanceof LayerConfig) {
 | 
			
		||||
            return this.applyLayer(result)
 | 
			
		||||
        }
 | 
			
		||||
        return this.applyFilter(result)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private async applyLayer(layer: LayerResult) {
 | 
			
		||||
    private async applyLayer(layer: LayerConfig) {
 | 
			
		||||
        for (const [name, otherLayer] of this.state.layerState.filteredLayers) {
 | 
			
		||||
            otherLayer.isDisplayed.setData(name === layer.osm_id)
 | 
			
		||||
            otherLayer.isDisplayed.setData(name === layer.id)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private async applyFilter(payload: FilterPayload) {
 | 
			
		||||
    private async applyFilter(payload: FilterSearchResult) {
 | 
			
		||||
        const state = this.state
 | 
			
		||||
 | 
			
		||||
        const { layer, filter, index } = payload
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -7,7 +7,6 @@
 | 
			
		|||
  import Translations from "./i18n/Translations"
 | 
			
		||||
  import Logo from "../assets/svg/Logo.svelte"
 | 
			
		||||
  import Tr from "./Base/Tr.svelte"
 | 
			
		||||
  import MoreScreen from "./BigComponents/MoreScreen"
 | 
			
		||||
  import LoginToggle from "./Base/LoginToggle.svelte"
 | 
			
		||||
  import Pencil from "../assets/svg/Pencil.svelte"
 | 
			
		||||
  import Constants from "../Models/Constants"
 | 
			
		||||
| 
						 | 
				
			
			@ -24,6 +23,8 @@
 | 
			
		|||
  import { ArrowTrendingUp } from "@babeard/svelte-heroicons/solid/ArrowTrendingUp"
 | 
			
		||||
  import Searchbar from "./Base/Searchbar.svelte"
 | 
			
		||||
  import ChevronDoubleRight from "@babeard/svelte-heroicons/mini/ChevronDoubleRight"
 | 
			
		||||
  import ThemeSearch from "../Logic/Search/ThemeSearch"
 | 
			
		||||
  import SearchUtils from "../Logic/Search/SearchUtils"
 | 
			
		||||
 | 
			
		||||
  const featureSwitches = new OsmConnectionFeatureSwitches()
 | 
			
		||||
  const osmConnection = new OsmConnection({
 | 
			
		||||
| 
						 | 
				
			
			@ -43,8 +44,8 @@
 | 
			
		|||
  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)
 | 
			
		||||
  const officialThemes: MinimalLayoutInformation[] = ThemeSearch.officialThemes.themes.filter(th => th.hideFromOverview === false)
 | 
			
		||||
  const hiddenThemes: MinimalLayoutInformation[] = ThemeSearch.officialThemes.themes.filter(th => th.hideFromOverview === true)
 | 
			
		||||
  let visitedHiddenThemes: Store<MinimalLayoutInformation[]> = UserRelatedState.initDiscoveredHiddenThemes(state.osmConnection)
 | 
			
		||||
    .map((knownIds) => hiddenThemes.filter((theme) =>
 | 
			
		||||
      knownIds.indexOf(theme.id) >= 0 || state.osmConnection.userDetails.data.name === "Pieter Vander Vennet"
 | 
			
		||||
| 
						 | 
				
			
			@ -60,7 +61,7 @@
 | 
			
		|||
      if (!search) {
 | 
			
		||||
        return themes
 | 
			
		||||
      }
 | 
			
		||||
      const scores = MoreScreen.sortedByLowest(search, themes)
 | 
			
		||||
      const scores = ThemeSearch.sortedByLowestScores(search, themes)
 | 
			
		||||
      const strict = scores.filter(sc => sc.lowest < 2)
 | 
			
		||||
      if (strict.length > 0) {
 | 
			
		||||
        return strict.map(sc => sc.theme)
 | 
			
		||||
| 
						 | 
				
			
			@ -84,7 +85,7 @@
 | 
			
		|||
  })
 | 
			
		||||
 | 
			
		||||
  function applySearch() {
 | 
			
		||||
    const didRedirect = MoreScreen.applySearch(search.data)
 | 
			
		||||
    const didRedirect = SearchUtils.applySpecialSearch(search.data)
 | 
			
		||||
    console.log("Did redirect?", didRedirect)
 | 
			
		||||
    if (didRedirect) {
 | 
			
		||||
      // Just for style and readability; won't _actually_ reach this
 | 
			
		||||
| 
						 | 
				
			
			@ -96,7 +97,7 @@
 | 
			
		|||
      return
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    window.location.href = MoreScreen.createUrlFor(candidate)
 | 
			
		||||
    window.location.href = ThemeSearch.createUrlFor(candidate, undefined)
 | 
			
		||||
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,194 +0,0 @@
 | 
			
		|||
import { MinimalLayoutInformation } from "../../Models/ThemeConfig/LayoutConfig"
 | 
			
		||||
import { Store } from "../../Logic/UIEventSource"
 | 
			
		||||
import { Utils } from "../../Utils"
 | 
			
		||||
import themeOverview from "../../assets/generated/theme_overview.json"
 | 
			
		||||
import Locale from "../i18n/Locale"
 | 
			
		||||
import { OsmConnection } from "../../Logic/Osm/OsmConnection"
 | 
			
		||||
 | 
			
		||||
export  type ThemeSearchScore = {
 | 
			
		||||
    theme: MinimalLayoutInformation,
 | 
			
		||||
    lowest: number,
 | 
			
		||||
    perLayer?: Record<string, number>,
 | 
			
		||||
    other: number
 | 
			
		||||
}
 | 
			
		||||
export default class MoreScreen {
 | 
			
		||||
    public static readonly officialThemes: {
 | 
			
		||||
        themes: MinimalLayoutInformation[],
 | 
			
		||||
        layers: Record<string, Record<string, string[]>>
 | 
			
		||||
    } = themeOverview
 | 
			
		||||
    public static readonly officialThemesById: Map<string, MinimalLayoutInformation> = new Map<string, MinimalLayoutInformation>()
 | 
			
		||||
    static {
 | 
			
		||||
        for (const th of MoreScreen.officialThemes.themes ?? []) {
 | 
			
		||||
            MoreScreen.officialThemesById.set(th.id, th)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /** 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 false
 | 
			
		||||
        }
 | 
			
		||||
        if (searchTerm === "personal") {
 | 
			
		||||
            window.location.href = MoreScreen.createUrlFor({ id: "personal" })
 | 
			
		||||
        }
 | 
			
		||||
        if (searchTerm === "bugs" || searchTerm === "issues") {
 | 
			
		||||
            window.location.href = "https://github.com/pietervdvn/MapComplete/issues"
 | 
			
		||||
        }
 | 
			
		||||
        if (searchTerm === "source") {
 | 
			
		||||
            window.location.href = "https://github.com/pietervdvn/MapComplete"
 | 
			
		||||
        }
 | 
			
		||||
        if (searchTerm === "docs") {
 | 
			
		||||
            window.location.href = "https://github.com/pietervdvn/MapComplete/tree/develop/Docs"
 | 
			
		||||
        }
 | 
			
		||||
        if (searchTerm === "osmcha" || searchTerm === "stats") {
 | 
			
		||||
            window.location.href = Utils.OsmChaLinkFor(7)
 | 
			
		||||
        }
 | 
			
		||||
        if (searchTerm === "studio") {
 | 
			
		||||
            window.location.href = "./studio.html"
 | 
			
		||||
        }
 | 
			
		||||
        return false
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Searches for the smallest distance in words; will split both the query and the terms
 | 
			
		||||
     *
 | 
			
		||||
     * MoreScreen.scoreKeywords("drinking water", {"en": ["A layer with drinking water points"]}, "en") // => 0
 | 
			
		||||
     * MoreScreen.scoreKeywords("waste", {"en": ["A layer with drinking water points"]}, "en") // => 2
 | 
			
		||||
     *
 | 
			
		||||
     */
 | 
			
		||||
    public static scoreKeywords(query: string, keywords: Record<string, string[]> | string[], language?: string): number {
 | 
			
		||||
        if(!keywords){
 | 
			
		||||
            return Infinity
 | 
			
		||||
        }
 | 
			
		||||
        language ??= Locale.language.data
 | 
			
		||||
        const queryParts = query.trim().split(" ").map(q => Utils.simplifyStringForSearch(q))
 | 
			
		||||
        let terms: string[]
 | 
			
		||||
        if (Array.isArray(keywords)) {
 | 
			
		||||
            terms = keywords
 | 
			
		||||
        } else {
 | 
			
		||||
            terms = (keywords[language] ?? []).concat(keywords["*"])
 | 
			
		||||
        }
 | 
			
		||||
        const termsAll = Utils.NoNullInplace(terms).flatMap(t => t.split(" "))
 | 
			
		||||
 | 
			
		||||
        let distanceSummed = 0
 | 
			
		||||
        for (let i = 0; i < queryParts.length; i++) {
 | 
			
		||||
            const q = queryParts[i]
 | 
			
		||||
            let minDistance: number = 99
 | 
			
		||||
            for (const term of termsAll) {
 | 
			
		||||
                const d = Utils.levenshteinDistance(q, Utils.simplifyStringForSearch(term))
 | 
			
		||||
                if (d < minDistance) {
 | 
			
		||||
                    minDistance = d
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            distanceSummed += minDistance
 | 
			
		||||
        }
 | 
			
		||||
        return distanceSummed
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static scoreLayers(query: string, layerWhitelist?: Set<string>): Record<string, number> {
 | 
			
		||||
        const result: Record<string, number> = {}
 | 
			
		||||
        for (const id in this.officialThemes.layers) {
 | 
			
		||||
            if(layerWhitelist !== undefined && !layerWhitelist.has(id)){
 | 
			
		||||
                continue
 | 
			
		||||
            }
 | 
			
		||||
            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
 | 
			
		||||
            }
 | 
			
		||||
            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
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    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 },
 | 
			
		||||
        state?: { layoutToUse?: { id } }
 | 
			
		||||
    ): string {
 | 
			
		||||
        if (layout === undefined) {
 | 
			
		||||
            return undefined
 | 
			
		||||
        }
 | 
			
		||||
        if (layout.id === undefined) {
 | 
			
		||||
            console.error("ID is undefined for layout", layout)
 | 
			
		||||
            return undefined
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (layout.id === state?.layoutToUse?.id) {
 | 
			
		||||
            return undefined
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        let path = window.location.pathname
 | 
			
		||||
        // Path starts with a '/' and contains everything, e.g. '/dir/dir/page.html'
 | 
			
		||||
        path = path.substr(0, path.lastIndexOf("/"))
 | 
			
		||||
        // Path will now contain '/dir/dir', or empty string in case of nothing
 | 
			
		||||
        if (path === "") {
 | 
			
		||||
            path = "."
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        let linkPrefix = `${path}/${layout.id.toLowerCase()}.html?`
 | 
			
		||||
        if (location.hostname === "localhost" || location.hostname === "127.0.0.1") {
 | 
			
		||||
            linkPrefix = `${path}/theme.html?layout=${layout.id}&`
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (layout.id.startsWith("http://") || layout.id.startsWith("https://")) {
 | 
			
		||||
            linkPrefix = `${path}/theme.html?userlayout=${layout.id}&`
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        return `${linkPrefix}`
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -48,7 +48,7 @@ export class BingRasterLayerProperties implements Partial<RasterLayerProperties>
 | 
			
		|||
        // "imageHeight": 256, "imageWidth": 256,
 | 
			
		||||
        // "imageUrlSubdomains": ["t0","t1","t2","t3"],
 | 
			
		||||
        // "zoomMax": 21,
 | 
			
		||||
        const imageryResource = metadata.resourceSets[0].resources[0]
 | 
			
		||||
        const imageryResource = metadata["resourceSets"][0].resources[0]
 | 
			
		||||
        const template = new URL(imageryResource.imageUrl)
 | 
			
		||||
        // Add tile image strictness param (n=)
 | 
			
		||||
        // • n=f -> (Fail) returns a 404
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,13 +1,16 @@
 | 
			
		|||
<script lang="ts">
 | 
			
		||||
  import type { SpecialVisualizationState } from "../SpecialVisualization"
 | 
			
		||||
  import Tr from "../Base/Tr.svelte"
 | 
			
		||||
  import type { FilterPayload, FilterResult, LayerResult } from "../../Logic/Search/GeocodingProvider"
 | 
			
		||||
  import { createEventDispatcher } from "svelte"
 | 
			
		||||
  import Icon from "../Map/Icon.svelte"
 | 
			
		||||
  import Marker from "../Map/Marker.svelte"
 | 
			
		||||
  import ToSvelte from "../Base/ToSvelte.svelte"
 | 
			
		||||
  import type { FilterSearchResult } from "../../Logic/Search/FilterSearch"
 | 
			
		||||
  import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
 | 
			
		||||
 | 
			
		||||
  export let entry: FilterResult | LayerResult
 | 
			
		||||
  export let entry: FilterSearchResult | LayerConfig
 | 
			
		||||
  let isLayer = entry instanceof LayerConfig
 | 
			
		||||
  let asLayer = <LayerConfig> entry
 | 
			
		||||
  let asFilter = <FilterSearchResult> entry
 | 
			
		||||
  export let state: SpecialVisualizationState
 | 
			
		||||
  let dispatch = createEventDispatcher<{ select }>()
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -20,16 +23,16 @@
 | 
			
		|||
<button on:click={() => apply()}>
 | 
			
		||||
  <div class="flex flex-col items-start">
 | 
			
		||||
    <div class="flex items-center gap-x-1">
 | 
			
		||||
      {#if entry.category === "layer"}
 | 
			
		||||
      {#if isLayer}
 | 
			
		||||
        <div class="w-8 h-8 p-1">
 | 
			
		||||
          <ToSvelte construct={entry.payload.defaultIcon()} />
 | 
			
		||||
          <ToSvelte construct={asLayer.defaultIcon()} />
 | 
			
		||||
        </div>
 | 
			
		||||
        <b>
 | 
			
		||||
          <Tr t={entry.payload.name} />
 | 
			
		||||
          <Tr t={asLayer.name} />
 | 
			
		||||
        </b>
 | 
			
		||||
      {:else}
 | 
			
		||||
        <Icon icon={entry.payload.option.icon ?? entry.payload. option.emoji} clss="w-4 h-4" emojiHeight="14px" />
 | 
			
		||||
        <Tr cls="whitespace-nowrap" t={entry.payload.option.question} />
 | 
			
		||||
        <Icon icon={asFilter.option.icon ?? asFilter.option.emoji} clss="w-4 h-4" emojiHeight="14px" />
 | 
			
		||||
        <Tr cls="whitespace-nowrap" t={asFilter.option.question} />
 | 
			
		||||
      {/if}
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										51
									
								
								src/UI/Search/FilterResults.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								src/UI/Search/FilterResults.svelte
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,51 @@
 | 
			
		|||
<script lang="ts">
 | 
			
		||||
  import { default as FilterResultSvelte } from "./FilterResult.svelte"
 | 
			
		||||
  import SidebarUnit from "../Base/SidebarUnit.svelte"
 | 
			
		||||
 | 
			
		||||
  import type { SpecialVisualizationState } from "../SpecialVisualization"
 | 
			
		||||
  import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
 | 
			
		||||
  import type { FilterSearchResult } from "../../Logic/Search/FilterSearch"
 | 
			
		||||
  import Tr from "../Base/Tr.svelte"
 | 
			
		||||
  import Translations from "../i18n/Translations"
 | 
			
		||||
 | 
			
		||||
  export let state: SpecialVisualizationState
 | 
			
		||||
  let searchTerm = state.searchState.searchTerm
 | 
			
		||||
  let activeLayers = state.layerState.activeLayers
 | 
			
		||||
  let filterResults = state.searchState.filterSuggestions
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
  let layerResults = state.searchState.layerSuggestions.map(layers => {
 | 
			
		||||
    const nowActive = activeLayers.data.filter(al => al.layerDef.isNormal())
 | 
			
		||||
    if (nowActive.length === 1) {
 | 
			
		||||
      const shownInActiveFiltersView = nowActive[0]
 | 
			
		||||
      layers = layers.filter(l => l.id !== shownInActiveFiltersView.layerDef.id)
 | 
			
		||||
    }
 | 
			
		||||
    return layers
 | 
			
		||||
  }, [activeLayers])
 | 
			
		||||
  let filterResultsClipped = filterResults.mapD(filters => {
 | 
			
		||||
    let layers = layerResults.data
 | 
			
		||||
    const ls: (FilterSearchResult | LayerConfig)[] = [].concat(layers, filters)
 | 
			
		||||
    if (ls.length <= 6) {
 | 
			
		||||
      return ls
 | 
			
		||||
    }
 | 
			
		||||
    return ls.slice(0, 4)
 | 
			
		||||
  }, [layerResults, activeLayers])
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
{#if $searchTerm.length > 0 && ($filterResults.length > 0 || $layerResults.length > 0)}
 | 
			
		||||
  <SidebarUnit>
 | 
			
		||||
 | 
			
		||||
    <h3><Tr t={Translations.t.general.search.pickFilter} /></h3>
 | 
			
		||||
 | 
			
		||||
    <div class="flex flex-wrap">
 | 
			
		||||
      {#each $filterResultsClipped as filterResult (filterResult)}
 | 
			
		||||
        <FilterResultSvelte {state} entry={filterResult} />
 | 
			
		||||
      {/each}
 | 
			
		||||
    </div>
 | 
			
		||||
    {#if $filterResults.length + $layerResults.length > $filterResultsClipped.length}
 | 
			
		||||
      <div class="flex justify-center">
 | 
			
		||||
        ... and {$filterResults.length + $layerResults.length - $filterResultsClipped.length} more ...
 | 
			
		||||
      </div>
 | 
			
		||||
    {/if}
 | 
			
		||||
  </SidebarUnit>
 | 
			
		||||
{/if}
 | 
			
		||||
							
								
								
									
										75
									
								
								src/UI/Search/GeocodeResults.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										75
									
								
								src/UI/Search/GeocodeResults.svelte
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,75 @@
 | 
			
		|||
<script lang="ts">
 | 
			
		||||
  /**
 | 
			
		||||
   * Shows all the location-results
 | 
			
		||||
   */
 | 
			
		||||
  import Translations from "../i18n/Translations"
 | 
			
		||||
  import { Store } from "../../Logic/UIEventSource"
 | 
			
		||||
  import SidebarUnit from "../Base/SidebarUnit.svelte"
 | 
			
		||||
  import type { SpecialVisualizationState } from "../SpecialVisualization"
 | 
			
		||||
  import Loading from "../Base/Loading.svelte"
 | 
			
		||||
  import { default as GeocodeResultSvelte } from "./GeocodeResult.svelte"
 | 
			
		||||
  import Tr from "../Base/Tr.svelte"
 | 
			
		||||
  import DotMenu from "../Base/DotMenu.svelte"
 | 
			
		||||
  import { CogIcon } from "@rgossiaux/svelte-heroicons/solid"
 | 
			
		||||
  import { TrashIcon } from "@babeard/svelte-heroicons/mini"
 | 
			
		||||
  import type { GeocodeResult } from "../../Logic/Search/GeocodingProvider"
 | 
			
		||||
 | 
			
		||||
  export let state: SpecialVisualizationState
 | 
			
		||||
 | 
			
		||||
  let searchTerm = state.searchState.searchTerm
 | 
			
		||||
  let results = state.searchState.suggestions
 | 
			
		||||
  let isSearching = state.searchState.suggestionsSearchRunning
 | 
			
		||||
  let recentlySeen: Store<GeocodeResult[]> = state.userRelatedState.recentlyVisitedSearch.value
 | 
			
		||||
const t = Translations.t.general.search
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
{#if $searchTerm.length > 0}
 | 
			
		||||
  <SidebarUnit>
 | 
			
		||||
 | 
			
		||||
    <h3><Tr t={t.locations}/></h3>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    {#if $results?.length > 0}
 | 
			
		||||
      {#each $results as entry (entry)}
 | 
			
		||||
        <GeocodeResultSvelte on:select {entry} {state} />
 | 
			
		||||
      {/each}
 | 
			
		||||
    {/if}
 | 
			
		||||
 | 
			
		||||
    {#if $isSearching}
 | 
			
		||||
      <div class="flex justify-center m-4 my-8">
 | 
			
		||||
        <Loading>
 | 
			
		||||
          <Tr t={t.searching} />
 | 
			
		||||
        </Loading>
 | 
			
		||||
      </div>
 | 
			
		||||
    {/if}
 | 
			
		||||
 | 
			
		||||
    {#if !$isSearching && $results.length === 0}
 | 
			
		||||
      <b class="flex justify-center p-4">
 | 
			
		||||
        <Tr t={t.nothingFor.Subs({term: "<i>"+$searchTerm+"</i>"})} />
 | 
			
		||||
      </b>
 | 
			
		||||
    {/if}
 | 
			
		||||
  </SidebarUnit>
 | 
			
		||||
 | 
			
		||||
{:else if $recentlySeen?.length > 0}
 | 
			
		||||
  <SidebarUnit>
 | 
			
		||||
    <div class="flex justify-between">
 | 
			
		||||
 | 
			
		||||
      <h3 class="m-2">
 | 
			
		||||
        <Tr t={t.recents} />
 | 
			
		||||
      </h3>
 | 
			
		||||
      <DotMenu>
 | 
			
		||||
        <button on:click={() => {state.userRelatedState.recentlyVisitedSearch.clear()}}>
 | 
			
		||||
          <TrashIcon />
 | 
			
		||||
          <Tr t={t.deleteSearchHistory}/>
 | 
			
		||||
        </button>
 | 
			
		||||
        <button on:click={() => state.guistate.openUsersettings("sync-visited-locations")}>
 | 
			
		||||
          <CogIcon />
 | 
			
		||||
          <Tr t={t.editSearchSyncSettings}/>
 | 
			
		||||
        </button>
 | 
			
		||||
      </DotMenu>
 | 
			
		||||
    </div>
 | 
			
		||||
    {#each $recentlySeen as entry (entry)}
 | 
			
		||||
      <GeocodeResultSvelte {entry} {state} on:select />
 | 
			
		||||
    {/each}
 | 
			
		||||
  </SidebarUnit>
 | 
			
		||||
{/if}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,19 +0,0 @@
 | 
			
		|||
<script lang="ts">
 | 
			
		||||
  import type { SearchResult } from "../../Logic/Search/GeocodingProvider"
 | 
			
		||||
 | 
			
		||||
  import ThemeResult from "../Search/ThemeResult.svelte"
 | 
			
		||||
  import FilterResult from "./FilterResult.svelte"
 | 
			
		||||
  import type { SpecialVisualizationState } from "../SpecialVisualization"
 | 
			
		||||
  import GeocodeResult from "./GeocodeResult.svelte"
 | 
			
		||||
 | 
			
		||||
  export let entry: SearchResult
 | 
			
		||||
  export let state: SpecialVisualizationState
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
{#if entry.category === "theme"}
 | 
			
		||||
  <ThemeResult entry={entry.payload} on:select  />
 | 
			
		||||
{:else if entry.category === "filter"}
 | 
			
		||||
  <FilterResult entry={entry.payload} {state} on:select />
 | 
			
		||||
{:else}
 | 
			
		||||
  <GeocodeResult {entry} {state} on:select />
 | 
			
		||||
{/if}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,181 +1,35 @@
 | 
			
		|||
<script lang="ts">
 | 
			
		||||
  import { Store } from "../../Logic/UIEventSource"
 | 
			
		||||
  import Loading from "../Base/Loading.svelte"
 | 
			
		||||
  import Tr from "../Base/Tr.svelte"
 | 
			
		||||
  import Translations from "../i18n/Translations"
 | 
			
		||||
  import { default as SearchResultSvelte } from "./SearchResult.svelte"
 | 
			
		||||
  import MoreScreen from "../BigComponents/MoreScreen"
 | 
			
		||||
  import type { FilterResult, GeocodeResult, LayerResult } from "../../Logic/Search/GeocodingProvider"
 | 
			
		||||
 | 
			
		||||
  import ActiveFilters from "./ActiveFilters.svelte"
 | 
			
		||||
  import Constants from "../../Models/Constants"
 | 
			
		||||
  import type { ActiveFilter } from "../../Logic/State/LayerState"
 | 
			
		||||
  import ThemeViewState from "../../Models/ThemeViewState"
 | 
			
		||||
  import {default as FilterResultSvelte} from "./FilterResult.svelte"
 | 
			
		||||
  import ThemeResult from "./ThemeResult.svelte"
 | 
			
		||||
  import SidebarUnit from "../Base/SidebarUnit.svelte"
 | 
			
		||||
  import { TrashIcon } from "@babeard/svelte-heroicons/mini"
 | 
			
		||||
  import DotMenu from "../Base/DotMenu.svelte"
 | 
			
		||||
  import { CogIcon } from "@rgossiaux/svelte-heroicons/solid"
 | 
			
		||||
   import ThemeResults from "./ThemeResults.svelte"
 | 
			
		||||
  import GeocodeResults from "./GeocodeResults.svelte"
 | 
			
		||||
  import FilterResults from "./FilterResults.svelte"
 | 
			
		||||
  import Tr from "../Base/Tr.svelte"
 | 
			
		||||
  import Translations from "../i18n/Translations"
 | 
			
		||||
 | 
			
		||||
  export let state: ThemeViewState
 | 
			
		||||
  let activeFilters: Store<ActiveFilter[]> = state.layerState.activeFilters.map(fs => fs.filter(f => Constants.priviliged_layers.indexOf(<any>f.layer.id) < 0))
 | 
			
		||||
  let recentlySeen: Store<GeocodeResult[]> = state.userRelatedState.recentlyVisitedSearch.value
 | 
			
		||||
  let recentThemes = state.userRelatedState.recentlyVisitedThemes.value.map(themes => themes.filter(th => th !== state.layout.id).slice(0, 6))
 | 
			
		||||
  let allowOtherThemes = state.featureSwitches.featureSwitchBackToThemeOverview
 | 
			
		||||
  let searchTerm = state.searchState.searchTerm
 | 
			
		||||
  let results = state.searchState.suggestions
 | 
			
		||||
  let isSearching = state.searchState.suggestionsSearchRunning
 | 
			
		||||
  let filterResults = state.searchState.filterSuggestions
 | 
			
		||||
  let activeLayers = state.layerState.activeLayers
 | 
			
		||||
  let layerResults = state.searchState.layerSuggestions.map(layers => {
 | 
			
		||||
    const nowActive = activeLayers.data.filter(al => al.layerDef.isNormal())
 | 
			
		||||
    if(nowActive.length === 1){
 | 
			
		||||
      const shownInActiveFiltersView = nowActive[0]
 | 
			
		||||
      layers = layers.filter(l => l.payload.id !== shownInActiveFiltersView.layerDef.id)
 | 
			
		||||
    }
 | 
			
		||||
    return layers
 | 
			
		||||
  }, [activeLayers])
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
  let filterResultsClipped = filterResults.mapD(filters => {
 | 
			
		||||
    let layers = layerResults.data
 | 
			
		||||
    const ls : (FilterResult | LayerResult)[] = [].concat(layers, filters)
 | 
			
		||||
    if (ls.length <= 8) {
 | 
			
		||||
      return ls
 | 
			
		||||
    }
 | 
			
		||||
    return ls.slice(0, 6)
 | 
			
		||||
  }, [layerResults, activeLayers])
 | 
			
		||||
  let themeResults = state.searchState.themeSuggestions
 | 
			
		||||
 | 
			
		||||
</script>
 | 
			
		||||
<div class="p-4 low-interaction flex gap-y-2 flex-col">
 | 
			
		||||
 | 
			
		||||
  <ActiveFilters {state} activeFilters={$activeFilters} />
 | 
			
		||||
 | 
			
		||||
  {#if $searchTerm.length === 0 && $filterResults.length === 0 && $activeFilters.length === 0 && $recentThemes.length === 0}
 | 
			
		||||
  {#if $searchTerm.length === 0 && $activeFilters.length === 0 }
 | 
			
		||||
    <div class="p-8 items-center text-center">
 | 
			
		||||
      <b>Use the search bar above to search for locations, filters and other maps</b>
 | 
			
		||||
      <b><Tr t={Translations.t.general.search.instructions}/></b>
 | 
			
		||||
    </div>
 | 
			
		||||
  {/if}
 | 
			
		||||
 | 
			
		||||
  {#if $searchTerm.length > 0 && ($filterResults.length > 0 || $layerResults.length > 0)}
 | 
			
		||||
    <SidebarUnit>
 | 
			
		||||
  <FilterResults {state}/>
 | 
			
		||||
 | 
			
		||||
      <h3>Pick a filter below</h3>
 | 
			
		||||
  <GeocodeResults {state}/>
 | 
			
		||||
 | 
			
		||||
      <div class="flex flex-wrap">
 | 
			
		||||
        {#each $filterResultsClipped as filterResult (filterResult)}
 | 
			
		||||
          <FilterResultSvelte {state} entry={filterResult} />
 | 
			
		||||
        {/each}
 | 
			
		||||
      </div>
 | 
			
		||||
      {#if $filterResults.length + $layerResults.length > $filterResultsClipped.length}
 | 
			
		||||
        <div class="flex justify-center">
 | 
			
		||||
          ... and {$filterResults.length + $layerResults.length - $filterResultsClipped.length} more ...
 | 
			
		||||
        </div>
 | 
			
		||||
  {#if $allowOtherThemes}
 | 
			
		||||
    <ThemeResults {state} />
 | 
			
		||||
  {/if}
 | 
			
		||||
    </SidebarUnit>
 | 
			
		||||
  {/if}
 | 
			
		||||
 | 
			
		||||
  <!-- Actual search results (or ""loading"", or ""no results"")-->
 | 
			
		||||
  {#if $searchTerm.length > 0}
 | 
			
		||||
    <SidebarUnit>
 | 
			
		||||
 | 
			
		||||
      <h3>Locations</h3>
 | 
			
		||||
 | 
			
		||||
      {#if $isSearching}
 | 
			
		||||
        <div class="flex justify-center m-4 my-8">
 | 
			
		||||
          <Loading />
 | 
			
		||||
        </div>
 | 
			
		||||
      {/if}
 | 
			
		||||
 | 
			
		||||
      {#if $results?.length > 0}
 | 
			
		||||
        {#each $results as entry (entry)}
 | 
			
		||||
          <SearchResultSvelte on:select {entry} {state} />
 | 
			
		||||
        {/each}
 | 
			
		||||
 | 
			
		||||
      {:else if !$isSearching}
 | 
			
		||||
        <b class="flex justify-center p-4">
 | 
			
		||||
          <Tr t={Translations.t.general.search.nothingFor.Subs({term: "<i>"+$searchTerm+"</i>"})} />
 | 
			
		||||
        </b>
 | 
			
		||||
      {/if}
 | 
			
		||||
    </SidebarUnit>
 | 
			
		||||
 | 
			
		||||
  {/if}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
  <!-- Other maps which match the search term-->
 | 
			
		||||
  {#if $themeResults.length > 0}
 | 
			
		||||
    <SidebarUnit>
 | 
			
		||||
      <h3>
 | 
			
		||||
        Other maps
 | 
			
		||||
      </h3>
 | 
			
		||||
      {#each $themeResults as entry (entry.id)}
 | 
			
		||||
        <ThemeResult {entry} />
 | 
			
		||||
      {/each}
 | 
			
		||||
    </SidebarUnit>
 | 
			
		||||
  {/if}
 | 
			
		||||
 | 
			
		||||
  {#if $searchTerm.length === 0 && $recentlySeen?.length === 0 && $recentThemes.length === 0}
 | 
			
		||||
    <SidebarUnit>
 | 
			
		||||
      <h3>
 | 
			
		||||
 | 
			
		||||
        Suggestions
 | 
			
		||||
      </h3>
 | 
			
		||||
 | 
			
		||||
    </SidebarUnit>
 | 
			
		||||
  {/if}
 | 
			
		||||
  {#if $searchTerm.length === 0 && $recentlySeen?.length > 0}
 | 
			
		||||
    <SidebarUnit>
 | 
			
		||||
      <div class="flex justify-between">
 | 
			
		||||
 | 
			
		||||
        <h3 class="m-2">
 | 
			
		||||
          <Tr t={Translations.t.general.search.recents} />
 | 
			
		||||
        </h3>
 | 
			
		||||
        <DotMenu>
 | 
			
		||||
          <button on:click={() => {state.userRelatedState.recentlyVisitedSearch.clear()}}>
 | 
			
		||||
            <TrashIcon />
 | 
			
		||||
            Delete search history
 | 
			
		||||
          </button>
 | 
			
		||||
          <button on:click={() => state.guistate.openUsersettings("sync-visited-locations")}>
 | 
			
		||||
            <CogIcon />
 | 
			
		||||
            Edit sync settings
 | 
			
		||||
          </button>
 | 
			
		||||
        </DotMenu>
 | 
			
		||||
      </div>
 | 
			
		||||
      {#each $recentlySeen as entry (entry)}
 | 
			
		||||
        <SearchResultSvelte {entry} {state} on:select />
 | 
			
		||||
      {/each}
 | 
			
		||||
    </SidebarUnit>
 | 
			
		||||
  {/if}
 | 
			
		||||
 | 
			
		||||
  {#if $searchTerm.length === 0 && $recentThemes?.length > 0 && $allowOtherThemes}
 | 
			
		||||
    <SidebarUnit>
 | 
			
		||||
      <div class="flex w-full justify-between">
 | 
			
		||||
 | 
			
		||||
        <h3 class="m-2">
 | 
			
		||||
          <Tr t={Translations.t.general.search.recentThemes} />
 | 
			
		||||
        </h3>
 | 
			
		||||
        <DotMenu>
 | 
			
		||||
          <button on:click={() => {state.userRelatedState.recentlyVisitedThemes.clear()}}>
 | 
			
		||||
            <TrashIcon />
 | 
			
		||||
            Delete earlier visited themes
 | 
			
		||||
          </button>
 | 
			
		||||
          <button on:click={() => state.guistate.openUsersettings("sync-visited-themes")}>
 | 
			
		||||
            <CogIcon />
 | 
			
		||||
            Edit sync settings
 | 
			
		||||
          </button>
 | 
			
		||||
        </DotMenu>
 | 
			
		||||
      </div>
 | 
			
		||||
      {#each $recentThemes as themeId (themeId)}
 | 
			
		||||
        <SearchResultSvelte
 | 
			
		||||
          entry={{payload: MoreScreen.officialThemesById.get(themeId), osm_id: themeId, category: "theme"}}
 | 
			
		||||
          {state}
 | 
			
		||||
          on:select />
 | 
			
		||||
      {/each}
 | 
			
		||||
 | 
			
		||||
    </SidebarUnit>
 | 
			
		||||
  {/if}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
</div>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,15 +1,15 @@
 | 
			
		|||
<script lang="ts">
 | 
			
		||||
  import { MinimalLayoutInformation } from "../../Models/ThemeConfig/LayoutConfig"
 | 
			
		||||
  import MoreScreen from "../BigComponents/MoreScreen"
 | 
			
		||||
  import { Translation } from "../i18n/Translation"
 | 
			
		||||
  import Icon from "../Map/Icon.svelte"
 | 
			
		||||
  import Tr from "../Base/Tr.svelte"
 | 
			
		||||
  import ThemeSearch from "../../Logic/Search/ThemeSearch"
 | 
			
		||||
 | 
			
		||||
  export let entry: MinimalLayoutInformation
 | 
			
		||||
  let otherTheme = entry
 | 
			
		||||
</script>
 | 
			
		||||
{#if entry}
 | 
			
		||||
  <a href={MoreScreen.createUrlFor(otherTheme)}
 | 
			
		||||
  <a href={ThemeSearch.createUrlFor(otherTheme)}
 | 
			
		||||
     class="flex items-center p-2 w-full gap-y-2 rounded-xl searchresult">
 | 
			
		||||
 | 
			
		||||
    <Icon icon={otherTheme.icon} clss="w-6 h-6 m-1" />
 | 
			
		||||
| 
						 | 
				
			
			@ -17,7 +17,6 @@
 | 
			
		|||
      <b>
 | 
			
		||||
        <Tr t={new Translation(otherTheme.title)} />
 | 
			
		||||
      </b>
 | 
			
		||||
      <!--<Tr t={new Translation(otherTheme.shortDescription)} /> -->
 | 
			
		||||
    </div>
 | 
			
		||||
  </a>
 | 
			
		||||
{/if}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										57
									
								
								src/UI/Search/ThemeResults.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								src/UI/Search/ThemeResults.svelte
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,57 @@
 | 
			
		|||
<script lang="ts">
 | 
			
		||||
  /**
 | 
			
		||||
   * Either shows the 'recent' themes (if search string is empty) or shows matching theme results
 | 
			
		||||
   */
 | 
			
		||||
  import Translations from "../i18n/Translations"
 | 
			
		||||
  import ThemeSearch from "../../Logic/Search/ThemeSearch"
 | 
			
		||||
  import SidebarUnit from "../Base/SidebarUnit.svelte"
 | 
			
		||||
  import ThemeResult from "./ThemeResult.svelte"
 | 
			
		||||
  import type { SpecialVisualizationState } from "../SpecialVisualization"
 | 
			
		||||
  import DotMenu from "../Base/DotMenu.svelte"
 | 
			
		||||
  import { TrashIcon } from "@babeard/svelte-heroicons/mini"
 | 
			
		||||
  import { CogIcon } from "@rgossiaux/svelte-heroicons/solid"
 | 
			
		||||
  import Tr from "../Base/Tr.svelte"
 | 
			
		||||
 | 
			
		||||
  export let state: SpecialVisualizationState
 | 
			
		||||
  let searchTerm = state.searchState.searchTerm
 | 
			
		||||
  let recentThemes = state.userRelatedState.recentlyVisitedThemes.value.map(themes => themes.filter(th => th !== state.layout.id).slice(0, 6))
 | 
			
		||||
  let themeResults = state.searchState.themeSuggestions
 | 
			
		||||
 | 
			
		||||
  const t =Translations.t.general.search
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
{#if $themeResults.length > 0}
 | 
			
		||||
  <SidebarUnit>
 | 
			
		||||
    <h3>
 | 
			
		||||
      <Tr t={t.otherMaps}/>
 | 
			
		||||
    </h3>
 | 
			
		||||
    {#each $themeResults as entry (entry.id)}
 | 
			
		||||
      <ThemeResult {entry} />
 | 
			
		||||
    {/each}
 | 
			
		||||
  </SidebarUnit>
 | 
			
		||||
{/if}
 | 
			
		||||
 | 
			
		||||
{#if $searchTerm.length === 0 && $recentThemes?.length > 0}
 | 
			
		||||
  <SidebarUnit>
 | 
			
		||||
    <div class="flex w-full justify-between">
 | 
			
		||||
 | 
			
		||||
      <h3 class="m-2">
 | 
			
		||||
        <Tr t={t.recentThemes} />
 | 
			
		||||
      </h3>
 | 
			
		||||
      <DotMenu>
 | 
			
		||||
        <button on:click={() => {state.userRelatedState.recentlyVisitedThemes.clear()}}>
 | 
			
		||||
          <TrashIcon />
 | 
			
		||||
          <Tr t={t.deleteThemeHistory}/>
 | 
			
		||||
        </button>
 | 
			
		||||
        <button on:click={() => state.guistate.openUsersettings("sync-visited-themes")}>
 | 
			
		||||
          <CogIcon />
 | 
			
		||||
          <Tr t={t.editThemeSync}/>
 | 
			
		||||
        </button>
 | 
			
		||||
      </DotMenu>
 | 
			
		||||
    </div>
 | 
			
		||||
    {#each $recentThemes as themeId (themeId)}
 | 
			
		||||
      <ThemeResult entry={ ThemeSearch.officialThemesById.get(themeId)} />
 | 
			
		||||
    {/each}
 | 
			
		||||
  </SidebarUnit>
 | 
			
		||||
{/if}
 | 
			
		||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue