Add layers to search menu

This commit is contained in:
Pieter Vander Vennet 2024-09-11 01:46:55 +02:00
parent e6dab1a83f
commit c591770eab
33 changed files with 332 additions and 195 deletions

View file

@ -109,7 +109,7 @@ Success: user sponteanously interacts with the questions!
> Then, the 'cuisine' was inspected. As the restaurant they visited is focusing on _vegetarian_ salads, the user wanted to use the freeform to enter 'vegetarian salad'
[ ] Failure: how to properly exp lain this? Move the 'vegetarian' question up? Should some options, such as 'chicken restaurant' be hidden if `vegetarian=only`?
[ ] Failure: how to properly explain this? Move the 'vegetarian' question up? Should some options, such as 'chicken restaurant' be hidden if `vegetarian=only`?
[ ] UI: issue: the emojis (especially flags) slightly overlaps with the text on this browser

View file

@ -24,6 +24,14 @@
"ca": "Una capa que mostra fonts d'aigua potable",
"cs": "Vrstva zobrazující fontány s pitnou vodou"
},
"searchTerms": {
"en": [
"drink","water","fountain","bubbler"
],
"nl": [
"drinken","water","drinkwater","waterfontein","fontein","kraan","kraantje"
]
},
"source": {
"osmTags": {
"and": [

View file

@ -40,7 +40,7 @@
]
}
},
"minzoom": 12,
"minzoom": 10,
"title": {
"render": {
"en": "Shop",

View file

@ -80,7 +80,7 @@
"override": {
"name=": null,
"minzoom": 17,
"filter": {
"=filter": {
"sameAs": "bike_shop"
}
}
@ -182,7 +182,7 @@
"builtin": "charging_station",
"override": {
"name": null,
"filter": {
"=filter": {
"sameAs": "charging_station_ebikes"
},
"minzoom": 18,
@ -209,7 +209,7 @@
"builtin": "vending_machine",
"override": {
"name": null,
"filter": {
"=filter": {
"sameAs": "vending_machine_bicycle"
},
"minzoom": 18,

View file

@ -56,7 +56,7 @@
"trolley_bay"
],
"overrideAll": {
"minzoom": 14,
"minzoom": 12,
"syncSelection": "theme-only"
}
}

View file

@ -5,6 +5,9 @@ import { Utils } from "../../Utils"
import Locale from "../../UI/i18n/Locale"
import Constants from "../../Models/Constants"
/**
* Searches matching filters
*/
export default class FilterSearch implements GeocodingProvider {
private readonly _state: SpecialVisualizationState
@ -13,18 +16,9 @@ export default class FilterSearch implements GeocodingProvider {
}
async search(query: string): Promise<SearchResult[]> {
return this.searchDirectlyWrapped(query)
return this.searchDirectly(query)
}
private searchDirectlyWrapped(query: string): FilterResult[] {
return this.searchDirectly(query).map(payload => ({
payload,
category: "filter",
osm_id: payload.layer.id + "/" + payload.filter.id + "/" + payload.option.osmTags?.asHumanString() ?? "none"
}))
}
public searchDirectly(query: string): FilterPayload[] {
public searchDirectly(query: string): FilterResult[] {
if (query.length === 0) {
return []
}
@ -34,11 +28,14 @@ export default class FilterSearch implements GeocodingProvider {
}
return query
}).filter(q => q.length > 0)
const possibleFilters: FilterPayload[] = []
const possibleFilters: FilterResult[] = []
for (const layer of this._state.layout.layers) {
if (!Array.isArray(layer.filters)) {
continue
}
if (layer.filterIsSameAs !== undefined) {
continue
}
for (const filter of layer.filters ?? []) {
for (let i = 0; i < filter.options.length; i++) {
const option = filter.options[i]
@ -64,7 +61,14 @@ export default class FilterSearch implements GeocodingProvider {
if (levehnsteinD > 0.25) {
continue
}
possibleFilters.push({ option, layer, filter, index: i })
possibleFilters.push(<FilterResult>{
category: "filter",
osm_id: layer.id + "/" + filter.id + "/" + i,
payload: {
option, layer, filter, index:
i,
},
})
}
}
}
@ -72,7 +76,7 @@ export default class FilterSearch implements GeocodingProvider {
}
suggest(query: string, options?: GeocodingOptions): Store<SearchResult[]> {
return new ImmutableStore(this.searchDirectlyWrapped(query))
return new ImmutableStore(this.searchDirectly(query))
}
@ -85,7 +89,7 @@ export default class FilterSearch implements GeocodingProvider {
if (!Array.isArray(filteredLayer.layerDef.filters)) {
continue
}
if (Constants.priviliged_layers.indexOf(id) >= 0) {
if (Constants.priviliged_layers.indexOf(<any> id) >= 0) {
continue
}
for (const filter of filteredLayer.layerDef.filters) {
@ -99,14 +103,14 @@ export default class FilterSearch implements GeocodingProvider {
option,
filter,
index: i,
layer: filteredLayer.layerDef
layer: filteredLayer.layerDef,
})
}
Utils.shuffle(singleFilterResults)
result.push(...singleFilterResults.slice(0,3))
result.push(...singleFilterResults.slice(0, 3))
}
}
Utils.shuffle(result)
return result.slice(0,6)
return result.slice(0, 6)
}
}

View file

@ -46,9 +46,11 @@ export type GeocodeResult = {
}
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 {

View file

@ -0,0 +1,53 @@
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"
export default class LayerSearch implements GeocodingProvider<LayerResult> {
private readonly _state: SpecialVisualizationState
private readonly _suggestionLimit: number
private readonly _layerWhitelist : Set<string>
constructor(state: SpecialVisualizationState, suggestionLimit: number) {
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)
}
suggest(query: string, options?: GeocodingOptions): Store<LayerResult[]> {
return new ImmutableStore(this.searchWrapped(query, this._suggestionLimit ?? 4))
}
private searchWrapped(query: string, limit: number): LayerResult[] {
return this.searchDirect(query, limit)
}
public searchDirect(query: string, limit: number): LayerResult[] {
if (query.length < 1) {
return []
}
const scores = MoreScreen.scoreLayers(query, this._layerWhitelist)
const asList:(LayerResult & {score:number})[] = []
for (const layer in scores) {
asList.push({
category: "layer",
payload: this._state.layout.getLayer(layer),
osm_id: layer,
score: scores[layer]
})
}
asList.sort((a, b) => a.score - b.score)
return asList
.filter(sorted => sorted.score < 2)
.slice(0, limit)
}
}

View file

@ -8,6 +8,7 @@ import Translations from "../../UI/i18n/Translations"
import { RegexTag } from "../Tags/RegexTag"
import { Or } from "../Tags/Or"
import FilterConfig from "../../Models/ThemeConfig/FilterConfig"
import Constants from "../../Models/Constants"
export type ActiveFilter = {
layer: LayerConfig,
@ -35,6 +36,10 @@ export default class LayerState {
private readonly _activeFilters: UIEventSource<ActiveFilter[]> = new UIEventSource([])
public readonly activeFilters: Store<ActiveFilter[]> = this._activeFilters
private readonly _activeLayers: UIEventSource<FilteredLayer[]> = new UIEventSource<FilteredLayer[]>(undefined)
public readonly activeLayers: Store<FilteredLayer[]> = this._activeLayers
private readonly _nonactiveLayers: UIEventSource<FilteredLayer[]> = new UIEventSource<FilteredLayer[]>(undefined)
public readonly nonactiveLayers: Store<FilteredLayer[]> = this._nonactiveLayers
private readonly osmConnection: OsmConnection
/**
@ -77,8 +82,16 @@ export default class LayerState {
private updateActiveFilters(){
const filters: ActiveFilter[] = []
const activeLayers: FilteredLayer[] = []
const nonactiveLayers: FilteredLayer[] = []
this.filteredLayers.forEach(fl => {
if(!fl.isDisplayed.data){
nonactiveLayers.push(fl)
return
}
activeLayers.push(fl)
if(fl.layerDef.filterIsSameAs){
return
}
for (const [filtername, appliedFilter] of fl.appliedFilters) {
@ -86,6 +99,7 @@ export default class LayerState {
continue
}
const filter = fl.layerDef.filters.find(f => f.id === filtername)
console.log("Updating active filters for flayer", fl.layerDef.id,"with filterconfig",filter)
if(typeof appliedFilter.data === "number"){
if(filter.options[appliedFilter.data].osmTags === undefined){
// This is probably the first, generic option which doesn't _actually_ filter
@ -99,6 +113,8 @@ export default class LayerState {
})
}
})
this._activeLayers.set(activeLayers)
this._nonactiveLayers.set(nonactiveLayers)
this._activeFilters.set(filters)
}

View file

@ -1,25 +1,26 @@
import GeocodingProvider, {
FilterPayload,
FilterPayload, FilterResult,
GeocodeResult,
GeocodingUtils,
type SearchResult
} from "../Geocoding/GeocodingProvider"
GeocodingUtils, LayerResult,
type SearchResult,
} from "../Search/GeocodingProvider"
import { ImmutableStore, Store, Stores, UIEventSource } from "../UIEventSource"
import CombinedSearcher from "../Geocoding/CombinedSearcher"
import FilterSearch from "../Geocoding/FilterSearch"
import LocalElementSearch from "../Geocoding/LocalElementSearch"
import CoordinateSearch from "../Geocoding/CoordinateSearch"
import ThemeSearch from "../Geocoding/ThemeSearch"
import OpenStreetMapIdSearch from "../Geocoding/OpenStreetMapIdSearch"
import PhotonSearch from "../Geocoding/PhotonSearch"
import CombinedSearcher from "../Search/CombinedSearcher"
import FilterSearch 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 "../Geocoding/GeocodingFeatureSource"
import GeocodingFeatureSource from "../Search/GeocodingFeatureSource"
import ShowDataLayer from "../../UI/Map/ShowDataLayer"
import LayerSearch from "../Search/LayerSearch"
export default class SearchState {
@ -28,8 +29,9 @@ export default class SearchState {
public readonly searchTerm: UIEventSource<string> = new UIEventSource<string>("")
public readonly searchIsFocused = new UIEventSource(false)
public readonly suggestions: Store<SearchResult[]>
public readonly filterSuggestions: Store<FilterPayload[]>
public readonly filterSuggestions: Store<FilterResult[]>
public readonly themeSuggestions: Store<MinimalLayoutInformation[]>
public readonly layerSuggestions: Store<LayerResult[]>
public readonly locationSearchers: ReadonlyArray<GeocodingProvider<GeocodeResult>>
private readonly state: ThemeViewState
@ -43,7 +45,7 @@ export default class SearchState {
// new LocalElementSearch(state, 5),
new CoordinateSearch(),
new OpenStreetMapIdSearch(state),
new PhotonSearch() // new NominatimGeocoding(),
new PhotonSearch(), // new NominatimGeocoding(),
]
const bounds = state.mapProperties.bounds
@ -53,33 +55,36 @@ export default class SearchState {
}
return this.locationSearchers.map(ls => ls.suggest(search, { bbox: bounds.data }))
}, [bounds]
}, [bounds],
)
this.suggestionsSearchRunning = suggestionsList.bind(suggestions => {
if(suggestions === undefined){
this.suggestionsSearchRunning = suggestionsList.bind(suggestions => {
if (suggestions === undefined) {
return new ImmutableStore(true)
}
return Stores.concat(suggestions).map(suggestions => suggestions.some(list => list === undefined))
})
this.suggestions = suggestionsList.bindD(suggestions =>
Stores.concat(suggestions).map(suggestions => CombinedSearcher.merge(suggestions))
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 layerSearch = new LayerSearch(state, 5)
this.layerSuggestions = this.searchTerm.mapD(query => layerSearch.searchDirect(query, 5))
const filterSearch = new FilterSearch(state)
this.filterSuggestions = this.searchTerm.stabilized(50).mapD(query => filterSearch.searchDirectly(query)
).mapD(filterResult => {
const active = state.layerState.activeFilters.data
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)
this.filterSuggestions = this.searchTerm.stabilized(50)
.mapD(query => filterSearch.searchDirectly(query))
.mapD(filterResult => {
const active = state.layerState.activeFilters.data
return filterResult.filter(({ payload: { filter, index, layer } }) => {
const foundMatch = active.some(active =>
active.filter.id === filter.id && layer.id === active.layer.id && active.control.data === index)
return !foundMatch
})
}, [state.layerState.activeFilters])
return !foundMatch
})
}, [state.layerState.activeFilters])
const geocodedFeatures = new GeocodingFeatureSource(this.suggestions.stabilized(250))
state.featureProperties.trackFeatureSource(geocodedFeatures)
@ -88,8 +93,8 @@ export default class SearchState {
{
layer: GeocodingUtils.searchLayer,
features: geocodedFeatures,
selectedElement: state.selectedElement
}
selectedElement: state.selectedElement,
},
)
this.showSearchDrawer = new UIEventSource(false)
@ -103,21 +108,31 @@ export default class SearchState {
}
public async apply(payload: FilterPayload) {
const state = this.state
const { layer, filter, index } = payload
const flayer = state.layerState.filteredLayers.get(layer.id)
const 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)
public async apply(result: FilterResult | LayerResult) {
if (result.category === "filter") {
return this.applyFilter(result.payload)
}
if (result.category === "layer") {
return this.applyLayer(result)
}
}
private async applyLayer(layer: LayerResult) {
for (const [name, otherLayer] of this.state.layerState.filteredLayers) {
otherLayer.isDisplayed.setData(name === layer.osm_id)
}
}
private async applyFilter(payload: FilterPayload) {
const state = this.state
const { layer, filter, index } = payload
for (const [name, otherLayer] of state.layerState.filteredLayers) {
otherLayer.isDisplayed.setData(name === layer.id)
}
const flayer = state.layerState.filteredLayers.get(layer.id)
flayer.isDisplayed.set(true)
const filtercontrol = flayer.appliedFilters.get(filter.id)
console.log("Could not apply", layer.id, ".", filter.id, index)
if (filtercontrol.data === index) {
filtercontrol.setData(undefined)
@ -126,76 +141,4 @@ export default class SearchState {
}
}
/**
* Tries to search and goto a given location
* Returns 'false' if search failed
*/
public async performSearch(): Promise<boolean> {
const query = this.searchTerm.data?.trim() ?? ""
if (query === "") {
return
}
const geolocationState = this.state.geolocation.geolocationState
const bounds = this.state.mapProperties.bounds
const bbox = this.state.mapProperties.bounds.data
try {
this.isSearching.set(true)
geolocationState?.allowMoving.setData(true)
geolocationState?.requestMoment.setData(undefined) // If the GPS is still searching for a fix, we say that we don't want tozoom to it anymore
let poi: SearchResult
if(this.suggestions.data){
poi = this.suggestions.data[0]
}else{
const results = GeocodingUtils.mergeSimilarResults([].concat(...await Promise.all(this.locationSearchers.map(ls => ls.search(query, { bbox: bounds.data })))))
poi = results[0]
}
if (poi.category === "theme") {
const theme = <MinimalLayoutInformation>poi.payload
const url = MoreScreen.createUrlFor(theme)
window.location = <any>url
return true
}
if (poi.category === "filter") {
await this.apply(poi.payload)
return true
}
if (poi.boundingbox) {
const [lat0, lat1, lon0, lon1] = poi.boundingbox
// Will trigger a 'fly to'
bounds.set(
new BBox([
[lon0, lat0],
[lon1, lat1]
]).pad(0.01)
)
} else if (poi.lon && poi.lat) {
this.state.mapProperties.flyTo(poi.lon, poi.lat, GeocodingUtils.categoryToZoomLevel[poi.category] ?? 16)
}
const perLayer = this.state.perLayer
if (perLayer !== undefined) {
const id = poi.osm_type + "/" + poi.osm_id
const layers = Array.from(perLayer?.values() ?? [])
for (const layer of layers) {
const found = layer.features.data.find((f) => f.properties.id === id)
if (found === undefined) {
continue
}
this.state.selectedElement?.setData(found)
}
}
return true
} catch (e) {
console.error(e)
this.feedback.set(Translations.t.general.search.error)
return false
} finally {
this.isSearching.set(false)
}
}
}

View file

@ -20,7 +20,7 @@ import { ThemeMetaTagging } from "./UserSettingsMetaTagging"
import { MapProperties } from "../../Models/MapProperties"
import Showdown from "showdown"
import { LocalStorageSource } from "../Web/LocalStorageSource"
import { GeocodeResult } from "../Geocoding/GeocodingProvider"
import { GeocodeResult } from "../Search/GeocodingProvider"
export class OptionallySyncedHistory<T> {
@ -42,8 +42,6 @@ export class OptionallySyncedHistory<T> {
"preference-" + key + "-history",
"sync",
)
console.log(">>>",key, this.syncPreference)
const synced = this.synced = UIEventSource.asObject<T[]>(osmconnection.GetLongPreference(key + "-history"), [])
const local = this.local = LocalStorageSource.GetParsed<T[]>(key + "-history", [])
const thisSession = this.thisSession = new UIEventSource<T[]>([], "optionally-synced:"+key+"(session only)")
@ -91,7 +89,6 @@ export class OptionallySyncedHistory<T> {
if (this._isSame) {
oldList = oldList.filter(x => !this._isSame(t, x))
}
console.log("Setting new history:", store, [t, ...oldList])
store.set([t, ...oldList].slice(0, this._maxHistory))
}
@ -198,7 +195,6 @@ export default class UserRelatedState {
this.showTags = this.osmConnection.GetPreference("show_tags")
this.showCrosshair = this.osmConnection.GetPreference("show_crosshair")
this.fixateNorth = this.osmConnection.GetPreference("fixate-north")
console.log("Fixate north is:", this.fixateNorth)
this.morePrivacy = this.osmConnection.GetPreference("more_privacy", "no")
this.a11y = this.osmConnection.GetPreference("a11y")

View file

@ -641,4 +641,18 @@ export default class LayerConfig extends WithContextLoader {
}
return mostShadowed ?? matchingPresets[0]
}
public isNormal(){
if(this.id.startsWith("note_import")){
return false
}
if(Constants.added_by_default.indexOf(<any> this.id) >=0){
return false
}
if(this.filterIsSameAs !== undefined){
return false
}
return true
}
}

View file

@ -67,7 +67,7 @@ import { LayerConfigJson } from "./ThemeConfig/Json/LayerConfigJson"
import Hash from "../Logic/Web/Hash"
import { GeoOperations } from "../Logic/GeoOperations"
import { CombinedFetcher } from "../Logic/Web/NearbyImagesSearch"
import { GeocodeResult, GeocodingUtils } from "../Logic/Geocoding/GeocodingProvider"
import { GeocodeResult, GeocodingUtils } from "../Logic/Search/GeocodingProvider"
import SearchState from "../Logic/State/SearchState"
/**

View file

@ -8,7 +8,7 @@
import Loading from "./Loading.svelte"
import { onDestroy } from "svelte"
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
import { GeocodingUtils } from "../../Logic/Geocoding/GeocodingProvider"
import { GeocodingUtils } from "../../Logic/Search/GeocodingProvider"
import ThemeViewState from "../../Models/ThemeViewState"
export let state: SpecialVisualizationState

View file

@ -66,7 +66,7 @@ export default class MoreScreen {
return Infinity
}
language ??= Locale.language.data
const queryParts = query.split(" ").map(q => Utils.simplifyStringForSearch(q))
const queryParts = query.trim().split(" ").map(q => Utils.simplifyStringForSearch(q))
let terms: string[]
if (Array.isArray(keywords)) {
terms = keywords
@ -90,9 +90,12 @@ export default class MoreScreen {
return distanceSummed
}
public static scoreLayers(query: string): Record<string, number> {
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

View file

@ -3,7 +3,7 @@
* Shows the current address when shaken
**/
import Motion from "../../Sensors/Motion"
import { NominatimGeocoding } from "../../Logic/Geocoding/NominatimGeocoding"
import { NominatimGeocoding } from "../../Logic/Search/NominatimGeocoding"
import Hotkeys from "../Base/Hotkeys"
import Translations from "../i18n/Translations"
import Locale from "../i18n/Locale"

View file

@ -1,8 +1,8 @@
<script lang="ts">
import type { ActiveFilter } from "../../Logic/State/LayerState"
import FilterOption from "./FilterOption.svelte"
import { XMarkIcon } from "@babeard/svelte-heroicons/mini"
import Loading from "../Base/Loading.svelte"
import FilterToggle from "./FilterToggle.svelte"
export let activeFilter: ActiveFilter
@ -21,10 +21,7 @@
{#if loading}
<Loading />
{:else }
<div class="badge button-unstyled w-fit">
<FilterToggle on:click={() => clear()}>
<FilterOption option={$option} />
<button on:click={() => clear()}>
<XMarkIcon class="w-5 h-5 pl-1" color="gray" />
</button>
</div>
</FilterToggle>
{/if}

View file

@ -3,13 +3,33 @@
import type { ActiveFilter } from "../../Logic/State/LayerState"
import Loading from "../Base/Loading.svelte"
import SidebarUnit from "../Base/SidebarUnit.svelte"
import type { SpecialVisualizationState } from "../SpecialVisualization"
import FilteredLayer from "../../Models/FilteredLayer"
import FilterToggle from "./FilterToggle.svelte"
import ToSvelte from "../Base/ToSvelte.svelte"
import Tr from "../Base/Tr.svelte"
import Constants from "../../Models/Constants"
import { Store } from "../../Logic/UIEventSource"
export let activeFilters: ActiveFilter[]
export let state: SpecialVisualizationState
let loading = false
let activeLayers: Store<FilteredLayer[]> = state.layerState.activeLayers.mapD(l => l.filter(l => l.layerDef.isNormal()))
let nonactiveLayers: Store<FilteredLayer[]> = state.layerState.nonactiveLayers.mapD(l => l.filter(l => l.layerDef.isNormal()))
function enableAllLayers() {
for (const flayer of $nonactiveLayers) {
flayer.isDisplayed.set(true)
}
}
function clear() {
loading = true
requestIdleCallback(() => {
enableAllLayers()
for (const activeFilter of activeFilters) {
activeFilter.control.setData(undefined)
@ -18,25 +38,52 @@
})
}
</script>
{#if activeFilters.length > 0}
{#if activeFilters.length > 0 || $activeLayers.length === 1 || $nonactiveLayers.length > 0}
<SidebarUnit>
<h3>Active filters</h3>
{#if loading}
<Loading />
{:else}
<div class="flex flex-wrap gap-x-1 gap-y-2">
{#each activeFilters as activeFilter (activeFilter)}
<div>
<ActiveFilterSvelte {activeFilter} />
</div>
{/each}
</div>
<div class="flex justify-between">
<h3>Active filters</h3>
<button class="as-link subtle self-end" on:click={() => clear()} style="margin-right: 0.75rem">
Clear filters
</button>
</div>
{#if loading}
<Loading />
{:else}
<div class="flex flex-wrap gap-x-1 gap-y-2 overflow-x-hidden overflow-y-auto">
{#if $activeLayers.length === 1}
<FilterToggle on:click={() => enableAllLayers()}>
<div class="w-8 h-8 p-1">
<ToSvelte construct={$activeLayers[0].layerDef.defaultIcon()} />
</div>
<b>
<Tr t={$activeLayers[0].layerDef.name} />
</b>
</FilterToggle>
{:else if $nonactiveLayers.length > 0}
{#each $nonactiveLayers as nonActive (nonActive.layerDef.id)}
<FilterToggle on:click={() => nonActive.isDisplayed.set(true)}>
<div class="w-8 h-8 p-1">
<ToSvelte construct={nonActive.layerDef.defaultIcon()} />
</div>
<del class="block-ruby">
<Tr t={nonActive.layerDef.name} />
</del>
</FilterToggle>
{/each}
{/if}
{#each activeFilters as activeFilter (activeFilter)}
<div>
<ActiveFilterSvelte {activeFilter} />
</div>
{/each}
</div>
{/if}
</SidebarUnit>

View file

@ -1,27 +1,36 @@
<script lang="ts">
import type { SpecialVisualizationState } from "../SpecialVisualization"
import Tr from "../Base/Tr.svelte"
import type { FilterPayload } from "../../Logic/Geocoding/GeocodingProvider"
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"
export let entry: FilterPayload
let { option } = entry
export let entry: FilterResult | LayerResult
export let state: SpecialVisualizationState
let dispatch = createEventDispatcher<{ select }>()
function apply() {
state.searchState.apply(entry)
dispatch("select")
state.searchState.apply(entry)
dispatch("select")
}
</script>
<button on:click={() => apply()}>
<div class="flex flex-col items-start">
<div class="flex items-center gap-x-1">
<Icon icon={option.icon ?? option.emoji} clss="w-4 h-4" emojiHeight="14px" />
<Tr cls="whitespace-nowrap" t={option.question} />
{#if entry.category === "layer"}
<div class="w-8 h-8 p-1">
<ToSvelte construct={entry.payload.defaultIcon()} />
</div>
<b>
<Tr t={entry.payload.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} />
{/if}
</div>
</div>
</button>

View file

@ -0,0 +1,9 @@
<script lang="ts">
import { XMarkIcon } from "@babeard/svelte-heroicons/mini"
</script>
<div class="badge button-unstyled w-fit">
<slot/>
<button on:click>
<XMarkIcon class="w-5 h-5 pl-1" color="gray" />
</button>
</div>

View file

@ -1,6 +1,6 @@
<script lang="ts">
import { GeocodingUtils } from "../../Logic/Geocoding/GeocodingProvider"
import type { GeocodeResult } from "../../Logic/Geocoding/GeocodingProvider"
import { GeocodingUtils } from "../../Logic/Search/GeocodingProvider"
import type { GeocodeResult } from "../../Logic/Search/GeocodingProvider"
import { GeoOperations } from "../../Logic/GeoOperations"
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
import { UIEventSource } from "../../Logic/UIEventSource"

View file

@ -1,5 +1,5 @@
<script lang="ts">
import type { SearchResult } from "../../Logic/Geocoding/GeocodingProvider"
import type { SearchResult } from "../../Logic/Search/GeocodingProvider"
import ThemeResult from "../Search/ThemeResult.svelte"
import FilterResult from "./FilterResult.svelte"

View file

@ -1,51 +1,79 @@
<script lang="ts">
import { Store, UIEventSource } from "../../Logic/UIEventSource"
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 { GeocodeResult } from "../../Logic/Geocoding/GeocodingProvider"
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 FilterResult from "./FilterResult.svelte"
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 { Dropdown, DropdownItem } from "flowbite-svelte"
import DotsCircleHorizontal from "@rgossiaux/svelte-heroicons/solid/DotsCircleHorizontal"
import DotMenu from "../Base/DotMenu.svelte"
import { CogIcon } from "@rgossiaux/svelte-heroicons/solid"
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.id !== state.layout.id).slice(0, 6))
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 activeFilters={$activeFilters} />
<ActiveFilters {state} activeFilters={$activeFilters} />
{#if $searchTerm.length > 0 && $filterResults.length > 0}
{#if $searchTerm.length === 0 && $filterResults.length === 0 && $activeFilters.length === 0 && $recentThemes.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>
</div>
{/if}
{#if $searchTerm.length > 0 && ($filterResults.length > 0 || $layerResults.length > 0)}
<SidebarUnit>
<h3>Pick a filter below</h3>
<div class="flex flex-wrap">
{#each $filterResults as filterResult (filterResult)}
<FilterResult {state} entry={filterResult} />
{#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}
@ -68,7 +96,7 @@
{:else if !$isSearching}
<b class="flex justify-center p-4">
<Tr t={Translations.t.general.search.nothingFor.Subs({term: $searchTerm})} />
<Tr t={Translations.t.general.search.nothingFor.Subs({term: "<i>"+$searchTerm+"</i>"})} />
</b>
{/if}
</SidebarUnit>
@ -88,8 +116,16 @@
</SidebarUnit>
{/if}
{#if $searchTerm.length === 0 && $recentlySeen?.length === 0 && $recentThemes.length === 0}
<SidebarUnit>
<h3>
{#if $searchTerm.length == 0 && $recentlySeen?.length > 0}
Suggestions
</h3>
</SidebarUnit>
{/if}
{#if $searchTerm.length === 0 && $recentlySeen?.length > 0}
<SidebarUnit>
<div class="flex justify-between">

View file

@ -326,7 +326,7 @@
<div
id="top-bar"
class="flex bg-black-light-transparent pointer-events-auto items-center justify-between px-4 py-1 flex-wrap-reverse">
class="flex bg-black-light-transparent pointer-events-auto items-center justify-between px-4 py-1 flex-wrap">
<!-- Top bar with tools -->
<div class="flex items-center">
@ -361,7 +361,7 @@
{/if}
<If condition={state.featureSwitches.featureSwitchSearch}>
<div class="flex items-center">
<div class="flex items-center flex-grow justify-end">
<div class="w-full sm:w-64">
<Searchbar value={state.searchState.searchTerm} isFocused={state.searchState.searchIsFocused} />
</div>

View file

@ -391,8 +391,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);
}