Search: similar filters in different layers are now merged, fix #
This commit is contained in:
parent
6ebc0632a3
commit
48186aa530
15 changed files with 156 additions and 63 deletions
|
@ -26,5 +26,5 @@ To validate the 'search with filters', the tester was tasked with searching all
|
|||
|
||||
## To improve
|
||||
|
||||
[ ] Why are there multiple "Open Now" filters?
|
||||
[x] Why are there multiple "Open Now" filters? Because of different layers with a similar filter, they are now shown merged
|
||||
[x] Special layers (e.g. gps location) are disabled as well (fixed now)
|
||||
|
|
|
@ -406,6 +406,7 @@
|
|||
"error": "Something went wrong…",
|
||||
"instructions": "Use the search bar above to search for locations, filters or other thematic maps",
|
||||
"locations": "Locations",
|
||||
"nMoreFilters": "{n} more",
|
||||
"nothing": "Nothing found…",
|
||||
"nothingFor": "No results found for {term}",
|
||||
"otherMaps": "Other maps",
|
||||
|
|
|
@ -7208,7 +7208,7 @@
|
|||
},
|
||||
"description": "Obchod",
|
||||
"filter": {
|
||||
"1": {
|
||||
"0": {
|
||||
"options": {
|
||||
"0": {
|
||||
"question": "Zobrazit pouze obchody prodávající použité zboží"
|
||||
|
|
|
@ -9087,7 +9087,7 @@
|
|||
},
|
||||
"description": "Ein Geschäft",
|
||||
"filter": {
|
||||
"1": {
|
||||
"0": {
|
||||
"options": {
|
||||
"0": {
|
||||
"question": "Nur Second-Hand-Geschäfte anzeigen"
|
||||
|
|
|
@ -9122,7 +9122,7 @@
|
|||
},
|
||||
"description": "A shop",
|
||||
"filter": {
|
||||
"1": {
|
||||
"0": {
|
||||
"options": {
|
||||
"0": {
|
||||
"question": "Only show shops selling second-hand items"
|
||||
|
|
|
@ -1188,14 +1188,14 @@ input[type="range"].range-lg::-moz-range-thumb {
|
|||
left: 25%;
|
||||
}
|
||||
|
||||
.bottom-4 {
|
||||
bottom: 1rem;
|
||||
}
|
||||
|
||||
.top-6 {
|
||||
top: 1.5rem;
|
||||
}
|
||||
|
||||
.bottom-4 {
|
||||
bottom: 1rem;
|
||||
}
|
||||
|
||||
.bottom-5 {
|
||||
bottom: 1.25rem;
|
||||
}
|
||||
|
@ -2760,6 +2760,10 @@ input[type="range"].range-lg::-moz-range-thumb {
|
|||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.overflow-x-hidden {
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.overflow-y-scroll {
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
@ -4719,6 +4723,11 @@ input[type="range"].range-lg::-moz-range-thumb {
|
|||
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
|
||||
}
|
||||
|
||||
.shadow-transparent {
|
||||
--tw-shadow-color: transparent;
|
||||
--tw-shadow: var(--tw-shadow-colored);
|
||||
}
|
||||
|
||||
.shadow-gray-300 {
|
||||
--tw-shadow-color: #D1D5DB;
|
||||
--tw-shadow: var(--tw-shadow-colored);
|
||||
|
@ -5353,8 +5362,8 @@ h2.group {
|
|||
align-items: center;
|
||||
white-space: nowrap;
|
||||
border-radius: 999rem;
|
||||
padding-left: 0.5rem;
|
||||
padding-right: 0.5rem;
|
||||
padding-left: 0.25rem;
|
||||
padding-right: 0.25rem;
|
||||
border: 1px solid var(--subtle-detail-color-light-contrast);
|
||||
background-color: var(--low-interaction-background);
|
||||
}
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import { SpecialVisualizationState } from "../../UI/SpecialVisualization"
|
||||
import { Utils } from "../../Utils"
|
||||
import Locale from "../../UI/i18n/Locale"
|
||||
import Constants from "../../Models/Constants"
|
||||
|
@ -110,4 +109,23 @@ export default class FilterSearch {
|
|||
Utils.shuffle(result)
|
||||
return result.slice(0, 6)
|
||||
}
|
||||
|
||||
/**
|
||||
* Partitions the list of filters in such a way that identically appearing filters will be in the same sublist.
|
||||
*
|
||||
* Note that this depends on the language and the displayed text. For example, two filters {"en": "A", "nl": "B"} and {"en": "X", "nl": "B"} will be joined for dutch but not for English
|
||||
*
|
||||
*/
|
||||
static mergeSemiIdenticalLayers<T extends FilterSearchResult = FilterSearchResult>(filters: ReadonlyArray<T>, language: string):T[][] {
|
||||
const results : Record<string, T[]> = {}
|
||||
for (const filter of filters) {
|
||||
const txt = filter.option.question.textFor(language)
|
||||
if(results[txt]){
|
||||
results[txt].push(filter)
|
||||
}else{
|
||||
results[txt] = [filter]
|
||||
}
|
||||
}
|
||||
return Object.values(results)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -79,7 +79,7 @@ export default class SearchState {
|
|||
return !foundMatch
|
||||
})
|
||||
}, [state.layerState.activeFilters])
|
||||
this.locationResults =new GeocodingFeatureSource(this.suggestions.stabilized(250))
|
||||
this.locationResults = new GeocodingFeatureSource(this.suggestions.stabilized(250))
|
||||
|
||||
this.showSearchDrawer = new UIEventSource(false)
|
||||
|
||||
|
@ -92,7 +92,7 @@ export default class SearchState {
|
|||
|
||||
}
|
||||
|
||||
public async apply(result: FilterSearchResult | LayerConfig) {
|
||||
public async apply(result: FilterSearchResult[] | LayerConfig) {
|
||||
if (result instanceof LayerConfig) {
|
||||
return this.applyLayer(result)
|
||||
}
|
||||
|
@ -105,17 +105,22 @@ export default class SearchState {
|
|||
}
|
||||
}
|
||||
|
||||
private async applyFilter(payload: FilterSearchResult) {
|
||||
private async applyFilter(payload: FilterSearchResult[]) {
|
||||
const state = this.state
|
||||
|
||||
const { layer, filter, index } = payload
|
||||
const layers = payload.map(fsr => fsr.layer.id)
|
||||
for (const [name, otherLayer] of state.layerState.filteredLayers) {
|
||||
const layer = otherLayer.layerDef
|
||||
if(!layer.isNormal()){
|
||||
if (!layer.isNormal()) {
|
||||
continue
|
||||
}
|
||||
otherLayer.isDisplayed.setData(payload.layer.id === layer.id)
|
||||
if(otherLayer.layerDef.minzoom > state.mapProperties.minzoom.data) {
|
||||
// Currently not displayed, we don't hide
|
||||
continue
|
||||
}
|
||||
otherLayer.isDisplayed.setData(layers.indexOf(layer.id) > 0)
|
||||
}
|
||||
for (const { filter, index, layer } of payload) {
|
||||
const flayer = state.layerState.filteredLayers.get(layer.id)
|
||||
flayer.isDisplayed.set(true)
|
||||
const filtercontrol = flayer.appliedFilters.get(filter.id)
|
||||
|
@ -125,9 +130,10 @@ export default class SearchState {
|
|||
filtercontrol.setData(index)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
closeIfFullscreen() {
|
||||
if(window.innerWidth < 640){
|
||||
if (window.innerWidth < 640) {
|
||||
this.showSearchDrawer.set(false)
|
||||
}
|
||||
}
|
||||
|
@ -135,7 +141,7 @@ export default class SearchState {
|
|||
clickedOnMap(feature: Feature) {
|
||||
const osmid = feature.properties.osm_id
|
||||
const localElement = this.state.indexedFeatures.featuresById.data.get(osmid)
|
||||
if(localElement){
|
||||
if (localElement) {
|
||||
this.state.selectedElement.set(localElement)
|
||||
return
|
||||
}
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
background: var(--background-color);
|
||||
padding: 0.5rem;
|
||||
border-radius: 0.5rem;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
:global(.sidebar-unit > h3) {
|
||||
|
|
|
@ -2,11 +2,16 @@
|
|||
import { Accordion, AccordionItem } from "flowbite-svelte"
|
||||
|
||||
export let expanded = false
|
||||
export let noBorder = false
|
||||
let defaultClass: string = undefined
|
||||
if(noBorder){
|
||||
defaultClass = "unstyled w-full flex-grow"
|
||||
}
|
||||
</script>
|
||||
|
||||
<Accordion>
|
||||
<AccordionItem open={expanded} paddingDefault="p-0" inactiveClass="text-black">
|
||||
<span slot="header" class="w-full p-2 text-base">
|
||||
<AccordionItem open={expanded} paddingDefault="p-0" inactiveClass="text-black" {defaultClass}>
|
||||
<span slot="header" class={!noBorder ? "w-full p-2 text-base" : "w-full"}>
|
||||
<slot name="header" />
|
||||
</span>
|
||||
<div class="low-interaction rounded-b p-2">
|
||||
|
|
|
@ -3,25 +3,36 @@
|
|||
import FilterOption from "./FilterOption.svelte"
|
||||
import Loading from "../Base/Loading.svelte"
|
||||
import FilterToggle from "./FilterToggle.svelte"
|
||||
import type { SpecialVisualizationState } from "../SpecialVisualization"
|
||||
|
||||
|
||||
export let activeFilter: ActiveFilter
|
||||
let { control, filter } = activeFilter
|
||||
export let activeFilter: ActiveFilter[]
|
||||
let { control, filter } = activeFilter[0]
|
||||
let option = control.map(c => filter.options[c] ?? filter.options[0])
|
||||
let loading = false
|
||||
|
||||
function clear() {
|
||||
loading = true
|
||||
requestIdleCallback(() => {
|
||||
control.setData(undefined)
|
||||
for (const af of activeFilter) {
|
||||
af.control.setData(undefined)
|
||||
}
|
||||
loading = false
|
||||
})
|
||||
}
|
||||
|
||||
export let state: SpecialVisualizationState
|
||||
let debug = state.featureSwitches.featureSwitchIsDebugging
|
||||
</script>
|
||||
{#if loading}
|
||||
<Loading />
|
||||
{:else }
|
||||
<FilterToggle on:click={() => clear()}>
|
||||
<FilterOption option={$option} />
|
||||
{#if $debug}
|
||||
<span class="subtle">
|
||||
({activeFilter.map(af => af.layer.id).join(", ")})
|
||||
</span>
|
||||
{/if}
|
||||
</FilterToggle>
|
||||
{/if}
|
||||
|
|
|
@ -8,10 +8,17 @@
|
|||
import FilterToggle from "./FilterToggle.svelte"
|
||||
import ToSvelte from "../Base/ToSvelte.svelte"
|
||||
import Tr from "../Base/Tr.svelte"
|
||||
import { Store } from "../../Logic/UIEventSource"
|
||||
import { Store, UIEventSource } from "../../Logic/UIEventSource"
|
||||
import Translations from "../i18n/Translations"
|
||||
import type { FilterSearchResult } from "../../Logic/Search/FilterSearch"
|
||||
import FilterSearch from "../../Logic/Search/FilterSearch"
|
||||
|
||||
export let activeFilters: ActiveFilter[]
|
||||
import Locale from "../i18n/Locale"
|
||||
|
||||
export let activeFilters: ( FilterSearchResult & ActiveFilter)[]
|
||||
let language = Locale.language
|
||||
let mergedActiveFilters = FilterSearch.mergeSemiIdenticalLayers(activeFilters, $language)
|
||||
$:mergedActiveFilters = FilterSearch.mergeSemiIdenticalLayers(activeFilters, $language)
|
||||
export let state: SpecialVisualizationState
|
||||
let loading = false
|
||||
const t =Translations.t.general.search
|
||||
|
@ -33,8 +40,6 @@
|
|||
loading = true
|
||||
requestIdleCallback(() => {
|
||||
enableAllLayers()
|
||||
|
||||
|
||||
for (const activeFilter of activeFilters) {
|
||||
activeFilter.control.setData(undefined)
|
||||
}
|
||||
|
@ -44,7 +49,7 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
{#if activeFilters.length > 0 || $nonactiveLayers.length > 0}
|
||||
{#if mergedActiveFilters.length > 0 || $nonactiveLayers.length > 0}
|
||||
<SidebarUnit>
|
||||
<div class="flex justify-between">
|
||||
<h3><Tr t={t.activeFilters}/></h3>
|
||||
|
@ -81,9 +86,9 @@
|
|||
{/if}
|
||||
|
||||
|
||||
{#each activeFilters as activeFilter (activeFilter)}
|
||||
{#each mergedActiveFilters as activeFilter (activeFilter)}
|
||||
<div>
|
||||
<ActiveFilterSvelte {activeFilter} />
|
||||
<ActiveFilterSvelte {activeFilter} {state}/>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
|
|
@ -8,13 +8,19 @@
|
|||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
|
||||
import Loading from "../Base/Loading.svelte"
|
||||
|
||||
export let entry: FilterSearchResult | LayerConfig
|
||||
let isLayer = entry instanceof LayerConfig
|
||||
let asLayer = <LayerConfig>entry
|
||||
let asFilter = <FilterSearchResult>entry
|
||||
export let entry: FilterSearchResult[] | LayerConfig
|
||||
let asFilter: FilterSearchResult[]
|
||||
let asLayer: LayerConfig
|
||||
if(Array.isArray(entry)){
|
||||
asFilter = entry
|
||||
}else{
|
||||
asLayer = <LayerConfig>entry
|
||||
|
||||
}
|
||||
export let state: SpecialVisualizationState
|
||||
|
||||
let loading = false
|
||||
let debug = state.featureSwitches.featureSwitchIsDebugging
|
||||
|
||||
function apply() {
|
||||
loading = true
|
||||
|
@ -34,7 +40,7 @@
|
|||
{/if}
|
||||
<div class="flex flex-col items-start">
|
||||
<div class="flex items-center gap-x-1">
|
||||
{#if isLayer}
|
||||
{#if asLayer}
|
||||
<div class="w-8 h-8 p-1">
|
||||
<ToSvelte construct={asLayer.defaultIcon()} />
|
||||
</div>
|
||||
|
@ -42,8 +48,11 @@
|
|||
<Tr t={asLayer.name} />
|
||||
</b>
|
||||
{:else}
|
||||
<Icon icon={asFilter.option.icon ?? asFilter.option.emoji} clss="w-4 h-4" emojiHeight="14px" />
|
||||
<Tr cls="whitespace-nowrap" t={asFilter.option.question} />
|
||||
<Icon icon={asFilter[0].option.icon ?? asFilter[0].option.emoji} clss="w-4 h-4" emojiHeight="14px" />
|
||||
<Tr cls="whitespace-nowrap" t={asFilter[0].option.question} />
|
||||
{#if $debug}
|
||||
<span class="subtle">({asFilter.map(f => f.layer.id).join(", ")})</span>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -4,15 +4,21 @@
|
|||
|
||||
import type { SpecialVisualizationState } from "../SpecialVisualization"
|
||||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
|
||||
import FilterSearch from "../../Logic/Search/FilterSearch"
|
||||
import type { FilterSearchResult } from "../../Logic/Search/FilterSearch"
|
||||
|
||||
import Tr from "../Base/Tr.svelte"
|
||||
import Translations from "../i18n/Translations"
|
||||
import Locale from "../i18n/Locale"
|
||||
import { Store } from "../../Logic/UIEventSource"
|
||||
import AccordionSingle from "../Flowbite/AccordionSingle.svelte"
|
||||
|
||||
export let state: SpecialVisualizationState
|
||||
let searchTerm = state.searchState.searchTerm
|
||||
let activeLayers = state.layerState.activeLayers
|
||||
let filterResults = state.searchState.filterSuggestions
|
||||
|
||||
let filtersMerged = filterResults.map(filters => FilterSearch.mergeSemiIdenticalLayers(filters, Locale.language.data), [Locale.language])
|
||||
|
||||
let layerResults = state.searchState.layerSuggestions.map(layers => {
|
||||
const nowActive = activeLayers.data.filter(al => al.layerDef.isNormal())
|
||||
|
@ -22,30 +28,44 @@
|
|||
}
|
||||
return layers
|
||||
}, [activeLayers])
|
||||
let filterResultsClipped = filterResults.mapD(filters => {
|
||||
let filterResultsClipped: Store<{
|
||||
clipped: (FilterSearchResult[] | LayerConfig)[],
|
||||
rest?: (FilterSearchResult[] | LayerConfig)[]
|
||||
}> = filtersMerged.mapD(filters => {
|
||||
let layers = layerResults.data
|
||||
const ls: (FilterSearchResult | LayerConfig)[] = [].concat(layers, filters)
|
||||
const ls: (FilterSearchResult[] | LayerConfig)[] = [].concat(layers, filters)
|
||||
if (ls.length <= 6) {
|
||||
return ls
|
||||
return { clipped: ls }
|
||||
}
|
||||
return ls.slice(0, 4)
|
||||
}, [layerResults, activeLayers])
|
||||
return { clipped: ls.slice(0, 4), rest: ls.slice(4) }
|
||||
}, [layerResults, activeLayers, Locale.language])
|
||||
</script>
|
||||
|
||||
{#if $searchTerm.length > 0 && ($filterResults.length > 0 || $layerResults.length > 0)}
|
||||
<SidebarUnit>
|
||||
|
||||
<h3><Tr t={Translations.t.general.search.pickFilter} /></h3>
|
||||
<h3>
|
||||
<Tr t={Translations.t.general.search.pickFilter} />
|
||||
</h3>
|
||||
|
||||
<div class="flex flex-wrap">
|
||||
{#each $filterResultsClipped as filterResult (filterResult)}
|
||||
{#each $filterResultsClipped.clipped 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 ...
|
||||
{#if $filtersMerged.length + $layerResults.length > $filterResultsClipped.clipped.length}
|
||||
<AccordionSingle noBorder>
|
||||
<div class="flex justify-end text-sm subtle" slot="header">
|
||||
<Tr t={Translations.t.general.search.nMoreFilters.Subs(
|
||||
{n: $filtersMerged.length + $layerResults.length - $filterResultsClipped.clipped.length}
|
||||
)}/>
|
||||
</div>
|
||||
<div class="flex flex-wrap overflow-y-auto">
|
||||
{#each $filterResultsClipped.rest as filterResult (filterResult)}
|
||||
<FilterResultSvelte {state} entry={filterResult} />
|
||||
{/each}
|
||||
</div>
|
||||
</AccordionSingle>
|
||||
{/if}
|
||||
</SidebarUnit>
|
||||
{/if}
|
||||
|
|
|
@ -9,11 +9,17 @@
|
|||
import FilterResults from "./FilterResults.svelte"
|
||||
import Tr from "../Base/Tr.svelte"
|
||||
import Translations from "../i18n/Translations"
|
||||
import type { FilterSearchResult } from "../../Logic/Search/FilterSearch"
|
||||
|
||||
export let state: ThemeViewState
|
||||
let activeFilters: Store<ActiveFilter[]> = state.layerState.activeFilters.map(fs => fs.filter(f =>
|
||||
let activeFilters: Store<(ActiveFilter & FilterSearchResult)[]> = state.layerState.activeFilters.map(fs => fs.filter(f =>
|
||||
(f.filter.options[0].fields.length === 0) &&
|
||||
Constants.priviliged_layers.indexOf(<any>f.layer.id) < 0))
|
||||
Constants.priviliged_layers.indexOf(<any>f.layer.id) < 0)
|
||||
.map(af => {
|
||||
const index = <number> af.control.data
|
||||
const r : FilterSearchResult & ActiveFilter = { ...af, index, option: af.filter.options[index] }
|
||||
return r
|
||||
}))
|
||||
let allowOtherThemes = state.featureSwitches.featureSwitchBackToThemeOverview
|
||||
let searchTerm = state.searchState.searchTerm
|
||||
</script>
|
||||
|
@ -23,13 +29,15 @@
|
|||
|
||||
{#if $searchTerm.length === 0 && $activeFilters.length === 0 }
|
||||
<div class="p-8 items-center text-center">
|
||||
<b><Tr t={Translations.t.general.search.instructions}/></b>
|
||||
<b>
|
||||
<Tr t={Translations.t.general.search.instructions} />
|
||||
</b>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<FilterResults {state}/>
|
||||
<FilterResults {state} />
|
||||
|
||||
<GeocodeResults {state}/>
|
||||
<GeocodeResults {state} />
|
||||
|
||||
{#if $allowOtherThemes}
|
||||
<ThemeResults {state} />
|
||||
|
|
Loading…
Reference in a new issue