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
|
|
@ -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}
|
||||
</SidebarUnit>
|
||||
{#if $allowOtherThemes}
|
||||
<ThemeResults {state} />
|
||||
{/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