forked from MapComplete/MapComplete
Add search for filters
This commit is contained in:
parent
1378c1a779
commit
c94393e825
24 changed files with 405 additions and 254 deletions
|
|
@ -14,6 +14,7 @@
|
|||
import Tr from "../Base/Tr.svelte"
|
||||
import Translations from "../i18n/Translations"
|
||||
import { Utils } from "../../Utils"
|
||||
import Icon from "../Map/Icon.svelte"
|
||||
|
||||
export let filteredLayer: FilteredLayer
|
||||
export let highlightedLayer: Store<string | undefined> = new ImmutableStore(undefined)
|
||||
|
|
@ -76,8 +77,8 @@
|
|||
<Dropdown value={getStateFor(filter)}>
|
||||
{#each filter.options as option, i}
|
||||
<option value={i}>
|
||||
{#if Utils.isEmoji(option.icon)}
|
||||
{option.icon}
|
||||
{#if option.emoji}
|
||||
{option.emoji}
|
||||
{/if}
|
||||
<Tr t={option.question} />
|
||||
</option>
|
||||
|
|
|
|||
|
|
@ -1,20 +1,20 @@
|
|||
<script lang="ts">
|
||||
import type { ActiveFilter } from "../../Logic/State/LayerState"
|
||||
import { Badge } from "flowbite-svelte"
|
||||
import type { SpecialVisualizationState } from "../SpecialVisualization"
|
||||
import Tr from "../Base/Tr.svelte"
|
||||
import Icon from "../Map/Icon.svelte"
|
||||
import { Badge } from "flowbite-svelte"
|
||||
import FilterOption from "./FilterOption.svelte"
|
||||
import { XMarkIcon } from "@babeard/svelte-heroicons/mini"
|
||||
|
||||
export let state: SpecialVisualizationState
|
||||
|
||||
export let activeFilter: ActiveFilter
|
||||
let { control, layer, filter } = activeFilter
|
||||
let option = control.map(c => {
|
||||
if (typeof c === "number") {
|
||||
return filter.options[c]
|
||||
}
|
||||
return filter.options[0]
|
||||
})
|
||||
let option = control.map(c => filter.options[c] ?? filter.options[0])
|
||||
</script>
|
||||
<Badge dismissable large border rounded color="dark" on:close={() =>{ console.log( "dismiss"); return control.setData(undefined) }}>
|
||||
<Tr cls="whitespace-nowrap" t={$option.question} />
|
||||
</Badge>
|
||||
<div class="badge">
|
||||
<FilterOption option={$option} />
|
||||
<button on:click={() => control.setData(undefined)}>
|
||||
|
||||
<XMarkIcon class="w-5 h-5 pl-1" color="gray" />
|
||||
</button>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,17 +1,24 @@
|
|||
<script lang="ts">
|
||||
import type { SpecialVisualizationState } from "../SpecialVisualization"
|
||||
import { Badge } from "flowbite-svelte"
|
||||
import ActiveFilter from "./ActiveFilter.svelte"
|
||||
|
||||
export let state: SpecialVisualizationState
|
||||
|
||||
let activeFilters = state.layerState.activeFilters
|
||||
import { default as ActiveFilterSvelte } from "./ActiveFilter.svelte"
|
||||
import type { ActiveFilter } from "../../Logic/State/LayerState"
|
||||
|
||||
export let activeFilters: ActiveFilter[]
|
||||
|
||||
function clear() {
|
||||
for (const activeFilter of activeFilters) {
|
||||
activeFilter.control.setData(undefined)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<div class="flex flex-wrap gap-y-1 gap-x-1 button-unstyled">
|
||||
{#if activeFilters.length > 0}
|
||||
<div class="flex flex-wrap gap-y-1 gap-x-1 button-unstyled">
|
||||
{#each activeFilters as activeFilter (activeFilter)}
|
||||
<ActiveFilterSvelte {activeFilter} />
|
||||
{/each}
|
||||
|
||||
{#each $activeFilters as activeFilter (activeFilter)}
|
||||
<ActiveFilter {activeFilter} {state} />
|
||||
{/each}
|
||||
</div>
|
||||
<button class="as-link subtle" on:click={() => clear()}>
|
||||
Clear filters
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/if}
|
||||
|
|
|
|||
10
src/UI/Search/FilterOption.svelte
Normal file
10
src/UI/Search/FilterOption.svelte
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
<script lang="ts">
|
||||
import type { FilterConfigOption } from "../../Models/ThemeConfig/FilterConfig"
|
||||
import Tr from "../Base/Tr.svelte"
|
||||
import Icon from "../Map/Icon.svelte"
|
||||
|
||||
export let option : FilterConfigOption
|
||||
</script>
|
||||
|
||||
<Icon icon={option.icon ?? option.emoji} clss="w-5 h-5" emojiHeight="14px" />
|
||||
<Tr t={option.question} />
|
||||
|
|
@ -1,14 +1,10 @@
|
|||
<script lang="ts">
|
||||
import type FilterConfig from "../../Models/ThemeConfig/FilterConfig"
|
||||
import type { FilterConfigOption } from "../../Models/ThemeConfig/FilterConfig"
|
||||
import type { SpecialVisualizationState } from "../SpecialVisualization"
|
||||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
|
||||
import Filter from "../../assets/svg/Filter.svelte"
|
||||
import Tr from "../Base/Tr.svelte"
|
||||
import type { FilterPayload } from "../../Logic/Geocoding/GeocodingProvider"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
import { FilterIcon as FilterSolid } from "@rgossiaux/svelte-heroicons/solid"
|
||||
import { FilterIcon as FilterOutline } from "@rgossiaux/svelte-heroicons/outline"
|
||||
import Icon from "../Map/Icon.svelte"
|
||||
import SearchResultUtils from "./SearchResultUtils"
|
||||
|
||||
export let entry: {
|
||||
category: "filter",
|
||||
|
|
@ -18,38 +14,19 @@
|
|||
export let state: SpecialVisualizationState
|
||||
let dispatch = createEventDispatcher<{ select }>()
|
||||
|
||||
let flayer = state.layerState.filteredLayers.get(layer.id)
|
||||
let filtercontrol = flayer.appliedFilters.get(filter.id)
|
||||
let isActive = filtercontrol.map(c => c === index)
|
||||
|
||||
function apply() {
|
||||
|
||||
for (const [name, otherLayer] of state.layerState.filteredLayers) {
|
||||
if(name === layer.id){
|
||||
otherLayer.isDisplayed.setData(true)
|
||||
continue
|
||||
}
|
||||
otherLayer.isDisplayed.setData(false)
|
||||
}
|
||||
|
||||
if(filtercontrol.data === index){
|
||||
filtercontrol.setData(undefined)
|
||||
}else{
|
||||
filtercontrol.setData(index)
|
||||
}
|
||||
SearchResultUtils.apply(entry.payload, state)
|
||||
dispatch("select")
|
||||
|
||||
|
||||
}
|
||||
</script>
|
||||
<button on:click={() => apply()}>
|
||||
{#if $isActive}
|
||||
<FilterSolid class="w-8 h-8 shrink-0" />
|
||||
{:else}
|
||||
<FilterOutline class="w-8 h-8 shrink-0" />
|
||||
{/if}
|
||||
<Tr t={option.question} />
|
||||
<div class="subtle">
|
||||
{layer.id}
|
||||
<div class="flex flex-col items-start">
|
||||
|
||||
<div class="flex items-center gap-x-1">
|
||||
<Icon icon={option.icon ?? option.emoji} clss="w-12 h-12 mr-2" emojiHeight="14px" />
|
||||
<Tr cls="whitespace-nowrap" t={option.question} />
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@
|
|||
import ThemeViewState from "../../Models/ThemeViewState"
|
||||
import GeocodingFeatureSource from "../../Logic/Geocoding/GeocodingFeatureSource"
|
||||
import MoreScreen from "../BigComponents/MoreScreen"
|
||||
import SearchResultUtils from "./SearchResultUtils"
|
||||
|
||||
export let perLayer: ReadonlyMap<string, GeoIndexedStoreForLayer> | undefined = undefined
|
||||
export let bounds: UIEventSource<BBox>
|
||||
|
|
@ -81,7 +82,6 @@
|
|||
return
|
||||
}
|
||||
const result = await searcher.search(searchContentsData, { bbox: bounds.data, limit: 10 })
|
||||
console.log("Results are", result)
|
||||
if (result.length == 0) {
|
||||
feedback = Translations.t.general.search.nothing.txt
|
||||
focusOnSearch()
|
||||
|
|
@ -91,11 +91,13 @@
|
|||
if (poi.category === "theme") {
|
||||
const theme = <MinimalLayoutInformation>poi.payload
|
||||
const url = MoreScreen.createUrlFor(theme, false)
|
||||
console.log("Found a theme, going to", url)
|
||||
// @ts-ignore
|
||||
window.location = url
|
||||
return
|
||||
}
|
||||
if(poi.category === "filter"){
|
||||
SearchResultUtils.apply(poi.payload, state)
|
||||
}
|
||||
if(poi.category === "filter"){
|
||||
return // Should not happen
|
||||
}
|
||||
|
|
@ -120,7 +122,6 @@
|
|||
continue
|
||||
}
|
||||
selectedElement?.setData(found)
|
||||
console.log("Found an element that probably matches:", selectedElement?.data)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
|
@ -146,7 +147,6 @@
|
|||
return Stores.holdDefined(bounds.bindD(bbox => searcher.suggest(search, { bbox, limit: 15 })))
|
||||
}
|
||||
)
|
||||
suggestions.addCallbackAndRun(suggestions => console.log(">>> suggestions are", suggestions))
|
||||
let geocededFeatures= new GeocodingFeatureSource(suggestions.stabilized(250))
|
||||
state.featureProperties.trackFeatureSource(geocededFeatures)
|
||||
|
||||
|
|
|
|||
|
|
@ -11,10 +11,9 @@
|
|||
</script>
|
||||
|
||||
{#if entry.category === "theme"}
|
||||
<ThemeResult {entry} />
|
||||
<ThemeResult {entry} on:select />
|
||||
{:else if entry.category === "filter"}
|
||||
<FilterResult {entry} {state} />
|
||||
<FilterResult {entry} {state} on:select />
|
||||
{:else}
|
||||
|
||||
<GeocodeResult {entry} {state} />
|
||||
<GeocodeResult {entry} {state} on:select />
|
||||
{/if}
|
||||
|
|
|
|||
25
src/UI/Search/SearchResultUtils.ts
Normal file
25
src/UI/Search/SearchResultUtils.ts
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
import { SpecialVisualizationState } from "../SpecialVisualization"
|
||||
import { FilterPayload } from "../../Logic/Geocoding/GeocodingProvider"
|
||||
|
||||
export default class SearchResultUtils {
|
||||
static apply(payload: FilterPayload, state: SpecialVisualizationState) {
|
||||
const { layer, filter, index, option } = payload
|
||||
|
||||
let flayer = state.layerState.filteredLayers.get(layer.id)
|
||||
let filtercontrol = flayer.appliedFilters.get(filter.id)
|
||||
|
||||
for (const [name, otherLayer] of state.layerState.filteredLayers) {
|
||||
if (name === layer.id) {
|
||||
otherLayer.isDisplayed.setData(true)
|
||||
continue
|
||||
}
|
||||
otherLayer.isDisplayed.setData(false)
|
||||
}
|
||||
|
||||
if (filtercontrol.data === index) {
|
||||
filtercontrol.setData(undefined)
|
||||
} else {
|
||||
filtercontrol.setData(index)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -8,14 +8,16 @@
|
|||
import MoreScreen from "../BigComponents/MoreScreen"
|
||||
import type { GeocodeResult, SearchResult } from "../../Logic/Geocoding/GeocodingProvider"
|
||||
import ActiveFilters from "./ActiveFilters.svelte"
|
||||
import Constants from "../../Models/Constants"
|
||||
import type { ActiveFilter } from "../../Logic/State/LayerState"
|
||||
|
||||
export let state: SpecialVisualizationState
|
||||
export let results: SearchResult[]
|
||||
export let searchTerm: Store<string>
|
||||
export let isFocused: UIEventSource<boolean>
|
||||
let hasActiveFilters = state.layerState.activeFilters.map(af => af.length > 0)
|
||||
let activeFilters: Store<ActiveFilter[]> = state.layerState.activeFilters.map(fs => fs.filter(f => Constants.priviliged_layers.indexOf(<any>f.layer.id) < 0))
|
||||
|
||||
console.log("Results are", results)
|
||||
let hasActiveFilters = activeFilters.map(afs => afs.length > 0)
|
||||
|
||||
let recentlySeen: Store<GeocodeResult[]> = state.recentlySearched.seenThisSession
|
||||
let recentThemes = state.userRelatedState.recentlyVisitedThemes.mapD(thms => thms.filter(th => th !== state.layout.id).slice(0, 3))
|
||||
|
|
@ -24,7 +26,7 @@
|
|||
|
||||
<div class="relative w-full h-full collapsable " class:collapsed={!$isFocused && !$hasActiveFilters}>
|
||||
<div class="searchbox normal-background">
|
||||
<ActiveFilters {state} />
|
||||
<ActiveFilters activeFilters={$activeFilters} />
|
||||
{#if $isFocused}
|
||||
{#if $searchTerm.length > 0 && results === undefined}
|
||||
<div class="flex justify-center m-4 my-8">
|
||||
|
|
@ -64,7 +66,7 @@
|
|||
</h3>
|
||||
{#each $recentThemes as themeId (themeId)}
|
||||
<SearchResultSvelte
|
||||
entry={{payload: MoreScreen.officialThemesById.get(themeId), display_name: themeId, lat: 0, lon: 0}}
|
||||
entry={{payload: MoreScreen.officialThemesById.get(themeId), osm_id: themeId, category: "theme"}}
|
||||
{state}
|
||||
on:select />
|
||||
{/each}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue