forked from MapComplete/MapComplete
		
	Add layers to search menu
This commit is contained in:
		
							parent
							
								
									e6dab1a83f
								
							
						
					
					
						commit
						c591770eab
					
				
					 33 changed files with 332 additions and 195 deletions
				
			
		|  | @ -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' | > 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 |  [ ] UI: issue: the emojis (especially flags) slightly overlaps with the text on this browser | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -24,6 +24,14 @@ | ||||||
|     "ca": "Una capa que mostra fonts d'aigua potable", |     "ca": "Una capa que mostra fonts d'aigua potable", | ||||||
|     "cs": "Vrstva zobrazující fontány s pitnou vodou" |     "cs": "Vrstva zobrazující fontány s pitnou vodou" | ||||||
|   }, |   }, | ||||||
|  |   "searchTerms": { | ||||||
|  |     "en": [ | ||||||
|  |       "drink","water","fountain","bubbler" | ||||||
|  |     ], | ||||||
|  |     "nl": [ | ||||||
|  |       "drinken","water","drinkwater","waterfontein","fontein","kraan","kraantje" | ||||||
|  |     ] | ||||||
|  |   }, | ||||||
|   "source": { |   "source": { | ||||||
|     "osmTags": { |     "osmTags": { | ||||||
|       "and": [ |       "and": [ | ||||||
|  |  | ||||||
|  | @ -40,7 +40,7 @@ | ||||||
|       ] |       ] | ||||||
|     } |     } | ||||||
|   }, |   }, | ||||||
|   "minzoom": 12, |   "minzoom": 10, | ||||||
|   "title": { |   "title": { | ||||||
|     "render": { |     "render": { | ||||||
|       "en": "Shop", |       "en": "Shop", | ||||||
|  |  | ||||||
|  | @ -80,7 +80,7 @@ | ||||||
|       "override": { |       "override": { | ||||||
|         "name=": null, |         "name=": null, | ||||||
|         "minzoom": 17, |         "minzoom": 17, | ||||||
|         "filter": { |         "=filter": { | ||||||
|           "sameAs": "bike_shop" |           "sameAs": "bike_shop" | ||||||
|         } |         } | ||||||
|       } |       } | ||||||
|  | @ -182,7 +182,7 @@ | ||||||
|       "builtin": "charging_station", |       "builtin": "charging_station", | ||||||
|       "override": { |       "override": { | ||||||
|         "name": null, |         "name": null, | ||||||
|         "filter": { |         "=filter": { | ||||||
|           "sameAs": "charging_station_ebikes" |           "sameAs": "charging_station_ebikes" | ||||||
|         }, |         }, | ||||||
|         "minzoom": 18, |         "minzoom": 18, | ||||||
|  | @ -209,7 +209,7 @@ | ||||||
|       "builtin": "vending_machine", |       "builtin": "vending_machine", | ||||||
|       "override": { |       "override": { | ||||||
|         "name": null, |         "name": null, | ||||||
|         "filter": { |         "=filter": { | ||||||
|           "sameAs": "vending_machine_bicycle" |           "sameAs": "vending_machine_bicycle" | ||||||
|         }, |         }, | ||||||
|         "minzoom": 18, |         "minzoom": 18, | ||||||
|  |  | ||||||
|  | @ -56,7 +56,7 @@ | ||||||
|     "trolley_bay" |     "trolley_bay" | ||||||
|   ], |   ], | ||||||
|   "overrideAll": { |   "overrideAll": { | ||||||
|     "minzoom": 14, |     "minzoom": 12, | ||||||
|     "syncSelection": "theme-only" |     "syncSelection": "theme-only" | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -5,6 +5,9 @@ import { Utils } from "../../Utils" | ||||||
| import Locale from "../../UI/i18n/Locale" | import Locale from "../../UI/i18n/Locale" | ||||||
| import Constants from "../../Models/Constants" | import Constants from "../../Models/Constants" | ||||||
| 
 | 
 | ||||||
|  | /** | ||||||
|  |  * Searches matching filters | ||||||
|  |  */ | ||||||
| export default class FilterSearch implements GeocodingProvider { | export default class FilterSearch implements GeocodingProvider { | ||||||
|     private readonly _state: SpecialVisualizationState |     private readonly _state: SpecialVisualizationState | ||||||
| 
 | 
 | ||||||
|  | @ -13,18 +16,9 @@ export default class FilterSearch implements GeocodingProvider { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     async search(query: string): Promise<SearchResult[]> { |     async search(query: string): Promise<SearchResult[]> { | ||||||
|         return this.searchDirectlyWrapped(query) |         return this.searchDirectly(query) | ||||||
|     } |     } | ||||||
| 
 |     public searchDirectly(query: string): FilterResult[] { | ||||||
|     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[] { |  | ||||||
|         if (query.length === 0) { |         if (query.length === 0) { | ||||||
|             return [] |             return [] | ||||||
|         } |         } | ||||||
|  | @ -34,11 +28,14 @@ export default class FilterSearch implements GeocodingProvider { | ||||||
|             } |             } | ||||||
|             return query |             return query | ||||||
|         }).filter(q => q.length > 0) |         }).filter(q => q.length > 0) | ||||||
|         const possibleFilters: FilterPayload[] = [] |         const possibleFilters: FilterResult[] = [] | ||||||
|         for (const layer of this._state.layout.layers) { |         for (const layer of this._state.layout.layers) { | ||||||
|             if (!Array.isArray(layer.filters)) { |             if (!Array.isArray(layer.filters)) { | ||||||
|                 continue |                 continue | ||||||
|             } |             } | ||||||
|  |             if (layer.filterIsSameAs !== undefined) { | ||||||
|  |                 continue | ||||||
|  |             } | ||||||
|             for (const filter of layer.filters ?? []) { |             for (const filter of layer.filters ?? []) { | ||||||
|                 for (let i = 0; i < filter.options.length; i++) { |                 for (let i = 0; i < filter.options.length; i++) { | ||||||
|                     const option = filter.options[i] |                     const option = filter.options[i] | ||||||
|  | @ -64,7 +61,14 @@ export default class FilterSearch implements GeocodingProvider { | ||||||
|                     if (levehnsteinD > 0.25) { |                     if (levehnsteinD > 0.25) { | ||||||
|                         continue |                         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[]> { |     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)) { |             if (!Array.isArray(filteredLayer.layerDef.filters)) { | ||||||
|                 continue |                 continue | ||||||
|             } |             } | ||||||
|             if (Constants.priviliged_layers.indexOf(id) >= 0) { |             if (Constants.priviliged_layers.indexOf(<any> id) >= 0) { | ||||||
|                 continue |                 continue | ||||||
|             } |             } | ||||||
|             for (const filter of filteredLayer.layerDef.filters) { |             for (const filter of filteredLayer.layerDef.filters) { | ||||||
|  | @ -99,14 +103,14 @@ export default class FilterSearch implements GeocodingProvider { | ||||||
|                         option, |                         option, | ||||||
|                         filter, |                         filter, | ||||||
|                         index: i, |                         index: i, | ||||||
|                         layer: filteredLayer.layerDef |                         layer: filteredLayer.layerDef, | ||||||
|                     }) |                     }) | ||||||
|                 } |                 } | ||||||
|                 Utils.shuffle(singleFilterResults) |                 Utils.shuffle(singleFilterResults) | ||||||
|                 result.push(...singleFilterResults.slice(0,3)) |                 result.push(...singleFilterResults.slice(0, 3)) | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|         Utils.shuffle(result) |         Utils.shuffle(result) | ||||||
|         return result.slice(0,6) |         return result.slice(0, 6) | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  | @ -46,9 +46,11 @@ export type GeocodeResult =  { | ||||||
| } | } | ||||||
| export type FilterPayload = { option: FilterConfigOption, filter: FilterConfig, layer: LayerConfig, index: number } | export type FilterPayload = { option: FilterConfigOption, filter: FilterConfig, layer: LayerConfig, index: number } | ||||||
| export type FilterResult =  { category: "filter", osm_id: string, payload:  FilterPayload } | export type FilterResult =  { category: "filter", osm_id: string, payload:  FilterPayload } | ||||||
|  | export type LayerResult = {category: "layer", osm_id: string, payload: LayerConfig} | ||||||
| export type SearchResult = | export type SearchResult = | ||||||
|     | FilterResult |     | FilterResult | ||||||
|     | { category: "theme", osm_id: string, payload: MinimalLayoutInformation } |     | { category: "theme", osm_id: string, payload: MinimalLayoutInformation } | ||||||
|  |     | LayerResult | ||||||
|     | GeocodeResult |     | GeocodeResult | ||||||
| 
 | 
 | ||||||
| export interface GeocodingOptions { | export interface GeocodingOptions { | ||||||
							
								
								
									
										53
									
								
								src/Logic/Search/LayerSearch.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								src/Logic/Search/LayerSearch.ts
									
										
									
									
									
										Normal 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) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | @ -8,6 +8,7 @@ import Translations from "../../UI/i18n/Translations" | ||||||
| import { RegexTag } from "../Tags/RegexTag" | import { RegexTag } from "../Tags/RegexTag" | ||||||
| import { Or } from "../Tags/Or" | import { Or } from "../Tags/Or" | ||||||
| import FilterConfig from "../../Models/ThemeConfig/FilterConfig" | import FilterConfig from "../../Models/ThemeConfig/FilterConfig" | ||||||
|  | import Constants from "../../Models/Constants" | ||||||
| 
 | 
 | ||||||
| export type ActiveFilter = { | export type ActiveFilter = { | ||||||
|     layer: LayerConfig, |     layer: LayerConfig, | ||||||
|  | @ -35,6 +36,10 @@ export default class LayerState { | ||||||
|     private readonly _activeFilters: UIEventSource<ActiveFilter[]> = new UIEventSource([]) |     private readonly _activeFilters: UIEventSource<ActiveFilter[]> = new UIEventSource([]) | ||||||
| 
 | 
 | ||||||
|     public readonly activeFilters: Store<ActiveFilter[]> = this._activeFilters |     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 |     private readonly osmConnection: OsmConnection | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|  | @ -77,8 +82,16 @@ export default class LayerState { | ||||||
| 
 | 
 | ||||||
|     private updateActiveFilters(){ |     private updateActiveFilters(){ | ||||||
|         const filters: ActiveFilter[] = [] |         const filters: ActiveFilter[] = [] | ||||||
|  |         const activeLayers: FilteredLayer[] = [] | ||||||
|  |         const nonactiveLayers: FilteredLayer[] =  [] | ||||||
|         this.filteredLayers.forEach(fl => { |         this.filteredLayers.forEach(fl => { | ||||||
|             if(!fl.isDisplayed.data){ |             if(!fl.isDisplayed.data){ | ||||||
|  |                 nonactiveLayers.push(fl) | ||||||
|  |                 return | ||||||
|  |             } | ||||||
|  |             activeLayers.push(fl) | ||||||
|  | 
 | ||||||
|  |             if(fl.layerDef.filterIsSameAs){ | ||||||
|                 return |                 return | ||||||
|             } |             } | ||||||
|             for (const [filtername, appliedFilter] of fl.appliedFilters) { |             for (const [filtername, appliedFilter] of fl.appliedFilters) { | ||||||
|  | @ -86,6 +99,7 @@ export default class LayerState { | ||||||
|                     continue |                     continue | ||||||
|                 } |                 } | ||||||
|                 const filter = fl.layerDef.filters.find(f => f.id === filtername) |                 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(typeof appliedFilter.data === "number"){ | ||||||
|                     if(filter.options[appliedFilter.data].osmTags === undefined){ |                     if(filter.options[appliedFilter.data].osmTags === undefined){ | ||||||
|                         // This is probably the first, generic option which doesn't _actually_ filter
 |                         // 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) |         this._activeFilters.set(filters) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,25 +1,26 @@ | ||||||
| import GeocodingProvider, { | import GeocodingProvider, { | ||||||
|     FilterPayload, |     FilterPayload, FilterResult, | ||||||
|     GeocodeResult, |     GeocodeResult, | ||||||
|     GeocodingUtils, |     GeocodingUtils, LayerResult, | ||||||
|     type SearchResult |     type SearchResult, | ||||||
| } from "../Geocoding/GeocodingProvider" | } from "../Search/GeocodingProvider" | ||||||
| import { ImmutableStore, Store, Stores, UIEventSource } from "../UIEventSource" | import { ImmutableStore, Store, Stores, UIEventSource } from "../UIEventSource" | ||||||
| import CombinedSearcher from "../Geocoding/CombinedSearcher" | import CombinedSearcher from "../Search/CombinedSearcher" | ||||||
| import FilterSearch from "../Geocoding/FilterSearch" | import FilterSearch from "../Search/FilterSearch" | ||||||
| import LocalElementSearch from "../Geocoding/LocalElementSearch" | import LocalElementSearch from "../Search/LocalElementSearch" | ||||||
| import CoordinateSearch from "../Geocoding/CoordinateSearch" | import CoordinateSearch from "../Search/CoordinateSearch" | ||||||
| import ThemeSearch from "../Geocoding/ThemeSearch" | import ThemeSearch from "../Search/ThemeSearch" | ||||||
| import OpenStreetMapIdSearch from "../Geocoding/OpenStreetMapIdSearch" | import OpenStreetMapIdSearch from "../Search/OpenStreetMapIdSearch" | ||||||
| import PhotonSearch from "../Geocoding/PhotonSearch" | import PhotonSearch from "../Search/PhotonSearch" | ||||||
| import ThemeViewState from "../../Models/ThemeViewState" | import ThemeViewState from "../../Models/ThemeViewState" | ||||||
| import Translations from "../../UI/i18n/Translations" | import Translations from "../../UI/i18n/Translations" | ||||||
| import type { MinimalLayoutInformation } from "../../Models/ThemeConfig/LayoutConfig" | import type { MinimalLayoutInformation } from "../../Models/ThemeConfig/LayoutConfig" | ||||||
| import MoreScreen from "../../UI/BigComponents/MoreScreen" | import MoreScreen from "../../UI/BigComponents/MoreScreen" | ||||||
| import { BBox } from "../BBox" | import { BBox } from "../BBox" | ||||||
| import { Translation } from "../../UI/i18n/Translation" | import { Translation } from "../../UI/i18n/Translation" | ||||||
| import GeocodingFeatureSource from "../Geocoding/GeocodingFeatureSource" | import GeocodingFeatureSource from "../Search/GeocodingFeatureSource" | ||||||
| import ShowDataLayer from "../../UI/Map/ShowDataLayer" | import ShowDataLayer from "../../UI/Map/ShowDataLayer" | ||||||
|  | import LayerSearch from "../Search/LayerSearch" | ||||||
| 
 | 
 | ||||||
| export default class SearchState { | export default class SearchState { | ||||||
| 
 | 
 | ||||||
|  | @ -28,8 +29,9 @@ export default class SearchState { | ||||||
|     public readonly searchTerm: UIEventSource<string> = new UIEventSource<string>("") |     public readonly searchTerm: UIEventSource<string> = new UIEventSource<string>("") | ||||||
|     public readonly searchIsFocused = new UIEventSource(false) |     public readonly searchIsFocused = new UIEventSource(false) | ||||||
|     public readonly suggestions: Store<SearchResult[]> |     public readonly suggestions: Store<SearchResult[]> | ||||||
|     public readonly filterSuggestions: Store<FilterPayload[]> |     public readonly filterSuggestions: Store<FilterResult[]> | ||||||
|     public readonly themeSuggestions: Store<MinimalLayoutInformation[]> |     public readonly themeSuggestions: Store<MinimalLayoutInformation[]> | ||||||
|  |     public readonly layerSuggestions: Store<LayerResult[]> | ||||||
|     public readonly locationSearchers: ReadonlyArray<GeocodingProvider<GeocodeResult>> |     public readonly locationSearchers: ReadonlyArray<GeocodingProvider<GeocodeResult>> | ||||||
| 
 | 
 | ||||||
|     private readonly state: ThemeViewState |     private readonly state: ThemeViewState | ||||||
|  | @ -43,7 +45,7 @@ export default class SearchState { | ||||||
|             // new LocalElementSearch(state, 5),
 |             // new LocalElementSearch(state, 5),
 | ||||||
|             new CoordinateSearch(), |             new CoordinateSearch(), | ||||||
|             new OpenStreetMapIdSearch(state), |             new OpenStreetMapIdSearch(state), | ||||||
|             new PhotonSearch() // new NominatimGeocoding(),
 |             new PhotonSearch(), // new NominatimGeocoding(),
 | ||||||
|         ] |         ] | ||||||
| 
 | 
 | ||||||
|         const bounds = state.mapProperties.bounds |         const bounds = state.mapProperties.bounds | ||||||
|  | @ -53,27 +55,30 @@ export default class SearchState { | ||||||
|                 } |                 } | ||||||
|                 return this.locationSearchers.map(ls => ls.suggest(search, { bbox: bounds.data })) |                 return this.locationSearchers.map(ls => ls.suggest(search, { bbox: bounds.data })) | ||||||
| 
 | 
 | ||||||
|             }, [bounds] |             }, [bounds], | ||||||
|         ) |         ) | ||||||
|         this.suggestionsSearchRunning = suggestionsList.bind(suggestions => { |         this.suggestionsSearchRunning = suggestionsList.bind(suggestions => { | ||||||
|             if(suggestions === undefined){ |             if (suggestions === undefined) { | ||||||
|                 return new ImmutableStore(true) |                 return new ImmutableStore(true) | ||||||
|             } |             } | ||||||
|             return Stores.concat(suggestions).map(suggestions => suggestions.some(list => list === undefined)) |             return Stores.concat(suggestions).map(suggestions => suggestions.some(list => list === undefined)) | ||||||
|         }) |         }) | ||||||
|         this.suggestions = suggestionsList.bindD(suggestions => |         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) |         const themeSearch = new ThemeSearch(state, 3) | ||||||
|         this.themeSuggestions = this.searchTerm.mapD(query => themeSearch.searchDirect(query, 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) |         const filterSearch = new FilterSearch(state) | ||||||
|         this.filterSuggestions = this.searchTerm.stabilized(50).mapD(query => filterSearch.searchDirectly(query) |         this.filterSuggestions = this.searchTerm.stabilized(50) | ||||||
|         ).mapD(filterResult => { |             .mapD(query => filterSearch.searchDirectly(query)) | ||||||
|  |             .mapD(filterResult => { | ||||||
|                 const active = state.layerState.activeFilters.data |                 const active = state.layerState.activeFilters.data | ||||||
|             return filterResult.filter(({ filter, index, layer }) => { |                 return filterResult.filter(({ payload: { filter, index, layer } }) => { | ||||||
|                     const foundMatch = active.some(active => |                     const foundMatch = active.some(active => | ||||||
|                         active.filter.id === filter.id && layer.id === active.layer.id && active.control.data === index) |                         active.filter.id === filter.id && layer.id === active.layer.id && active.control.data === index) | ||||||
| 
 | 
 | ||||||
|  | @ -88,8 +93,8 @@ export default class SearchState { | ||||||
|             { |             { | ||||||
|                 layer: GeocodingUtils.searchLayer, |                 layer: GeocodingUtils.searchLayer, | ||||||
|                 features: geocodedFeatures, |                 features: geocodedFeatures, | ||||||
|                 selectedElement: state.selectedElement |                 selectedElement: state.selectedElement, | ||||||
|             } |             }, | ||||||
|         ) |         ) | ||||||
| 
 | 
 | ||||||
|         this.showSearchDrawer = new UIEventSource(false) |         this.showSearchDrawer = new UIEventSource(false) | ||||||
|  | @ -103,21 +108,31 @@ export default class SearchState { | ||||||
| 
 | 
 | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     public async apply(result: FilterResult | LayerResult) { | ||||||
|  |         if (result.category === "filter") { | ||||||
|  |             return this.applyFilter(result.payload) | ||||||
|  |         } | ||||||
|  |         if (result.category === "layer") { | ||||||
|  |             return this.applyLayer(result) | ||||||
|  |         } | ||||||
|  |     } | ||||||
| 
 | 
 | ||||||
|     public async apply(payload: FilterPayload) { |     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 state = this.state | ||||||
|  | 
 | ||||||
|         const { layer, filter, index } = payload |         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) { |         for (const [name, otherLayer] of state.layerState.filteredLayers) { | ||||||
|             if (name === layer.id) { |             otherLayer.isDisplayed.setData(name === layer.id) | ||||||
|                 otherLayer.isDisplayed.setData(true) |  | ||||||
|                 continue |  | ||||||
|         } |         } | ||||||
|             otherLayer.isDisplayed.setData(false) |         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) |         console.log("Could not apply", layer.id, ".", filter.id, index) | ||||||
|         if (filtercontrol.data === index) { |         if (filtercontrol.data === index) { | ||||||
|             filtercontrol.setData(undefined) |             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) |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -20,7 +20,7 @@ import { ThemeMetaTagging } from "./UserSettingsMetaTagging" | ||||||
| import { MapProperties } from "../../Models/MapProperties" | import { MapProperties } from "../../Models/MapProperties" | ||||||
| import Showdown from "showdown" | import Showdown from "showdown" | ||||||
| import { LocalStorageSource } from "../Web/LocalStorageSource" | import { LocalStorageSource } from "../Web/LocalStorageSource" | ||||||
| import { GeocodeResult } from "../Geocoding/GeocodingProvider" | import { GeocodeResult } from "../Search/GeocodingProvider" | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| export class OptionallySyncedHistory<T> { | export class OptionallySyncedHistory<T> { | ||||||
|  | @ -42,8 +42,6 @@ export class OptionallySyncedHistory<T> { | ||||||
|             "preference-" + key + "-history", |             "preference-" + key + "-history", | ||||||
|             "sync", |             "sync", | ||||||
|         ) |         ) | ||||||
|         console.log(">>>",key, this.syncPreference) |  | ||||||
| 
 |  | ||||||
|         const synced = this.synced = UIEventSource.asObject<T[]>(osmconnection.GetLongPreference(key + "-history"), []) |         const synced = this.synced = UIEventSource.asObject<T[]>(osmconnection.GetLongPreference(key + "-history"), []) | ||||||
|         const local = this.local = LocalStorageSource.GetParsed<T[]>(key + "-history", []) |         const local = this.local = LocalStorageSource.GetParsed<T[]>(key + "-history", []) | ||||||
|         const thisSession = this.thisSession = new UIEventSource<T[]>([], "optionally-synced:"+key+"(session only)") |         const thisSession = this.thisSession = new UIEventSource<T[]>([], "optionally-synced:"+key+"(session only)") | ||||||
|  | @ -91,7 +89,6 @@ export class OptionallySyncedHistory<T> { | ||||||
|         if (this._isSame) { |         if (this._isSame) { | ||||||
|             oldList = oldList.filter(x => !this._isSame(t, x)) |             oldList = oldList.filter(x => !this._isSame(t, x)) | ||||||
|         } |         } | ||||||
|         console.log("Setting new history:", store, [t, ...oldList]) |  | ||||||
|         store.set([t, ...oldList].slice(0, this._maxHistory)) |         store.set([t, ...oldList].slice(0, this._maxHistory)) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | @ -198,7 +195,6 @@ export default class UserRelatedState { | ||||||
|         this.showTags = this.osmConnection.GetPreference("show_tags") |         this.showTags = this.osmConnection.GetPreference("show_tags") | ||||||
|         this.showCrosshair = this.osmConnection.GetPreference("show_crosshair") |         this.showCrosshair = this.osmConnection.GetPreference("show_crosshair") | ||||||
|         this.fixateNorth = this.osmConnection.GetPreference("fixate-north") |         this.fixateNorth = this.osmConnection.GetPreference("fixate-north") | ||||||
|         console.log("Fixate north is:", this.fixateNorth) |  | ||||||
|         this.morePrivacy = this.osmConnection.GetPreference("more_privacy", "no") |         this.morePrivacy = this.osmConnection.GetPreference("more_privacy", "no") | ||||||
| 
 | 
 | ||||||
|         this.a11y = this.osmConnection.GetPreference("a11y") |         this.a11y = this.osmConnection.GetPreference("a11y") | ||||||
|  |  | ||||||
|  | @ -641,4 +641,18 @@ export default class LayerConfig extends WithContextLoader { | ||||||
|         } |         } | ||||||
|         return mostShadowed ?? matchingPresets[0] |         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 | ||||||
|  |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -67,7 +67,7 @@ import { LayerConfigJson } from "./ThemeConfig/Json/LayerConfigJson" | ||||||
| import Hash from "../Logic/Web/Hash" | import Hash from "../Logic/Web/Hash" | ||||||
| import { GeoOperations } from "../Logic/GeoOperations" | import { GeoOperations } from "../Logic/GeoOperations" | ||||||
| import { CombinedFetcher } from "../Logic/Web/NearbyImagesSearch" | 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" | import SearchState from "../Logic/State/SearchState" | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  |  | ||||||
|  | @ -8,7 +8,7 @@ | ||||||
|   import Loading from "./Loading.svelte" |   import Loading from "./Loading.svelte" | ||||||
|   import { onDestroy } from "svelte" |   import { onDestroy } from "svelte" | ||||||
|   import LayerConfig from "../../Models/ThemeConfig/LayerConfig" |   import LayerConfig from "../../Models/ThemeConfig/LayerConfig" | ||||||
|   import { GeocodingUtils } from "../../Logic/Geocoding/GeocodingProvider" |   import { GeocodingUtils } from "../../Logic/Search/GeocodingProvider" | ||||||
|   import ThemeViewState from "../../Models/ThemeViewState" |   import ThemeViewState from "../../Models/ThemeViewState" | ||||||
| 
 | 
 | ||||||
|   export let state: SpecialVisualizationState |   export let state: SpecialVisualizationState | ||||||
|  |  | ||||||
|  | @ -66,7 +66,7 @@ export default class MoreScreen { | ||||||
|             return Infinity |             return Infinity | ||||||
|         } |         } | ||||||
|         language ??= Locale.language.data |         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[] |         let terms: string[] | ||||||
|         if (Array.isArray(keywords)) { |         if (Array.isArray(keywords)) { | ||||||
|             terms = keywords |             terms = keywords | ||||||
|  | @ -90,9 +90,12 @@ export default class MoreScreen { | ||||||
|         return distanceSummed |         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> = {} |         const result: Record<string, number> = {} | ||||||
|         for (const id in this.officialThemes.layers) { |         for (const id in this.officialThemes.layers) { | ||||||
|  |             if(layerWhitelist !== undefined && !layerWhitelist.has(id)){ | ||||||
|  |                 continue | ||||||
|  |             } | ||||||
|             const keywords = this.officialThemes.layers[id] |             const keywords = this.officialThemes.layers[id] | ||||||
|             const distance = this.scoreKeywords(query, keywords) |             const distance = this.scoreKeywords(query, keywords) | ||||||
|             result[id] = distance |             result[id] = distance | ||||||
|  |  | ||||||
|  | @ -3,7 +3,7 @@ | ||||||
|    * Shows the current address when shaken |    * Shows the current address when shaken | ||||||
|    **/ |    **/ | ||||||
|   import Motion from "../../Sensors/Motion" |   import Motion from "../../Sensors/Motion" | ||||||
|   import { NominatimGeocoding } from "../../Logic/Geocoding/NominatimGeocoding" |   import { NominatimGeocoding } from "../../Logic/Search/NominatimGeocoding" | ||||||
|   import Hotkeys from "../Base/Hotkeys" |   import Hotkeys from "../Base/Hotkeys" | ||||||
|   import Translations from "../i18n/Translations" |   import Translations from "../i18n/Translations" | ||||||
|   import Locale from "../i18n/Locale" |   import Locale from "../i18n/Locale" | ||||||
|  |  | ||||||
|  | @ -1,8 +1,8 @@ | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
|   import type { ActiveFilter } from "../../Logic/State/LayerState" |   import type { ActiveFilter } from "../../Logic/State/LayerState" | ||||||
|   import FilterOption from "./FilterOption.svelte" |   import FilterOption from "./FilterOption.svelte" | ||||||
|   import { XMarkIcon } from "@babeard/svelte-heroicons/mini" |  | ||||||
|   import Loading from "../Base/Loading.svelte" |   import Loading from "../Base/Loading.svelte" | ||||||
|  |   import FilterToggle from "./FilterToggle.svelte" | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|   export let activeFilter: ActiveFilter |   export let activeFilter: ActiveFilter | ||||||
|  | @ -21,10 +21,7 @@ | ||||||
| {#if loading} | {#if loading} | ||||||
|   <Loading /> |   <Loading /> | ||||||
| {:else } | {:else } | ||||||
|   <div class="badge button-unstyled w-fit"> |   <FilterToggle  on:click={() => clear()}> | ||||||
|     <FilterOption option={$option} /> |     <FilterOption option={$option} /> | ||||||
|     <button on:click={() => clear()}> |   </FilterToggle> | ||||||
|       <XMarkIcon class="w-5 h-5 pl-1" color="gray" /> |  | ||||||
|     </button> |  | ||||||
|   </div> |  | ||||||
| {/if} | {/if} | ||||||
|  |  | ||||||
|  | @ -3,13 +3,33 @@ | ||||||
|   import type { ActiveFilter } from "../../Logic/State/LayerState" |   import type { ActiveFilter } from "../../Logic/State/LayerState" | ||||||
|   import Loading from "../Base/Loading.svelte" |   import Loading from "../Base/Loading.svelte" | ||||||
|   import SidebarUnit from "../Base/SidebarUnit.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 activeFilters: ActiveFilter[] | ||||||
|  |   export let state: SpecialVisualizationState | ||||||
|   let loading = false |   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() { |   function clear() { | ||||||
|     loading = true |     loading = true | ||||||
|     requestIdleCallback(() => { |     requestIdleCallback(() => { | ||||||
|  |       enableAllLayers() | ||||||
|  | 
 | ||||||
| 
 | 
 | ||||||
|       for (const activeFilter of activeFilters) { |       for (const activeFilter of activeFilters) { | ||||||
|         activeFilter.control.setData(undefined) |         activeFilter.control.setData(undefined) | ||||||
|  | @ -18,14 +38,43 @@ | ||||||
|     }) |     }) | ||||||
|   } |   } | ||||||
| </script> | </script> | ||||||
| {#if activeFilters.length > 0} | 
 | ||||||
|  | {#if activeFilters.length > 0 || $activeLayers.length === 1 || $nonactiveLayers.length > 0} | ||||||
|   <SidebarUnit> |   <SidebarUnit> | ||||||
|  |     <div class="flex justify-between"> | ||||||
|       <h3>Active filters</h3> |       <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} |     {#if loading} | ||||||
|       <Loading /> |       <Loading /> | ||||||
|     {:else} |     {:else} | ||||||
|       <div class="flex flex-wrap gap-x-1 gap-y-2"> | 
 | ||||||
|  |       <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)} |         {#each activeFilters as activeFilter (activeFilter)} | ||||||
|           <div> |           <div> | ||||||
|  | @ -34,9 +83,7 @@ | ||||||
|         {/each} |         {/each} | ||||||
|       </div> |       </div> | ||||||
| 
 | 
 | ||||||
|       <button class="as-link subtle self-end" on:click={() => clear()} style="margin-right: 0.75rem"> | 
 | ||||||
|         Clear filters |  | ||||||
|       </button> |  | ||||||
|     {/if} |     {/if} | ||||||
|   </SidebarUnit> |   </SidebarUnit> | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,12 +1,13 @@ | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
|   import type { SpecialVisualizationState } from "../SpecialVisualization" |   import type { SpecialVisualizationState } from "../SpecialVisualization" | ||||||
|   import Tr from "../Base/Tr.svelte" |   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 { createEventDispatcher } from "svelte" | ||||||
|   import Icon from "../Map/Icon.svelte" |   import Icon from "../Map/Icon.svelte" | ||||||
|  |   import Marker from "../Map/Marker.svelte" | ||||||
|  |   import ToSvelte from "../Base/ToSvelte.svelte" | ||||||
| 
 | 
 | ||||||
|   export let entry: FilterPayload |   export let entry: FilterResult | LayerResult | ||||||
|   let { option } = entry |  | ||||||
|   export let state: SpecialVisualizationState |   export let state: SpecialVisualizationState | ||||||
|   let dispatch = createEventDispatcher<{ select }>() |   let dispatch = createEventDispatcher<{ select }>() | ||||||
| 
 | 
 | ||||||
|  | @ -18,10 +19,18 @@ | ||||||
| </script> | </script> | ||||||
| <button on:click={() => apply()}> | <button on:click={() => apply()}> | ||||||
|   <div class="flex flex-col items-start"> |   <div class="flex flex-col items-start"> | ||||||
| 
 |  | ||||||
|     <div class="flex items-center gap-x-1"> |     <div class="flex items-center gap-x-1"> | ||||||
|       <Icon icon={option.icon ?? option.emoji} clss="w-4 h-4" emojiHeight="14px" /> |       {#if entry.category === "layer"} | ||||||
|       <Tr cls="whitespace-nowrap" t={option.question} /> |         <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> | ||||||
|   </div> |   </div> | ||||||
| </button> | </button> | ||||||
|  |  | ||||||
							
								
								
									
										9
									
								
								src/UI/Search/FilterToggle.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								src/UI/Search/FilterToggle.svelte
									
										
									
									
									
										Normal 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> | ||||||
|  | @ -1,6 +1,6 @@ | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
|   import { GeocodingUtils } from "../../Logic/Geocoding/GeocodingProvider" |   import { GeocodingUtils } from "../../Logic/Search/GeocodingProvider" | ||||||
|   import type { GeocodeResult } from "../../Logic/Geocoding/GeocodingProvider" |   import type { GeocodeResult } from "../../Logic/Search/GeocodingProvider" | ||||||
|   import { GeoOperations } from "../../Logic/GeoOperations" |   import { GeoOperations } from "../../Logic/GeoOperations" | ||||||
|   import LayerConfig from "../../Models/ThemeConfig/LayerConfig" |   import LayerConfig from "../../Models/ThemeConfig/LayerConfig" | ||||||
|   import { UIEventSource } from "../../Logic/UIEventSource" |   import { UIEventSource } from "../../Logic/UIEventSource" | ||||||
|  |  | ||||||
|  | @ -1,5 +1,5 @@ | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
|   import type { SearchResult } from "../../Logic/Geocoding/GeocodingProvider" |   import type { SearchResult } from "../../Logic/Search/GeocodingProvider" | ||||||
| 
 | 
 | ||||||
|   import ThemeResult from "../Search/ThemeResult.svelte" |   import ThemeResult from "../Search/ThemeResult.svelte" | ||||||
|   import FilterResult from "./FilterResult.svelte" |   import FilterResult from "./FilterResult.svelte" | ||||||
|  |  | ||||||
|  | @ -1,51 +1,79 @@ | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
|   import { Store, UIEventSource } from "../../Logic/UIEventSource" |   import { Store } from "../../Logic/UIEventSource" | ||||||
|   import Loading from "../Base/Loading.svelte" |   import Loading from "../Base/Loading.svelte" | ||||||
|   import Tr from "../Base/Tr.svelte" |   import Tr from "../Base/Tr.svelte" | ||||||
|   import Translations from "../i18n/Translations" |   import Translations from "../i18n/Translations" | ||||||
|   import { default as SearchResultSvelte } from "./SearchResult.svelte" |   import { default as SearchResultSvelte } from "./SearchResult.svelte" | ||||||
|   import MoreScreen from "../BigComponents/MoreScreen" |   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 ActiveFilters from "./ActiveFilters.svelte" | ||||||
|   import Constants from "../../Models/Constants" |   import Constants from "../../Models/Constants" | ||||||
|   import type { ActiveFilter } from "../../Logic/State/LayerState" |   import type { ActiveFilter } from "../../Logic/State/LayerState" | ||||||
|   import ThemeViewState from "../../Models/ThemeViewState" |   import ThemeViewState from "../../Models/ThemeViewState" | ||||||
|   import FilterResult from "./FilterResult.svelte" |   import {default as FilterResultSvelte} from "./FilterResult.svelte" | ||||||
|   import ThemeResult from "./ThemeResult.svelte" |   import ThemeResult from "./ThemeResult.svelte" | ||||||
|   import SidebarUnit from "../Base/SidebarUnit.svelte" |   import SidebarUnit from "../Base/SidebarUnit.svelte" | ||||||
|   import { TrashIcon } from "@babeard/svelte-heroicons/mini" |   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 DotMenu from "../Base/DotMenu.svelte" | ||||||
|   import { CogIcon } from "@rgossiaux/svelte-heroicons/solid" |   import { CogIcon } from "@rgossiaux/svelte-heroicons/solid" | ||||||
| 
 | 
 | ||||||
|   export let state: ThemeViewState |   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 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 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 allowOtherThemes = state.featureSwitches.featureSwitchBackToThemeOverview | ||||||
|   let searchTerm = state.searchState.searchTerm |   let searchTerm = state.searchState.searchTerm | ||||||
|   let results = state.searchState.suggestions |   let results = state.searchState.suggestions | ||||||
|   let isSearching = state.searchState.suggestionsSearchRunning |   let isSearching = state.searchState.suggestionsSearchRunning | ||||||
|   let filterResults = state.searchState.filterSuggestions |   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 |   let themeResults = state.searchState.themeSuggestions | ||||||
| 
 | 
 | ||||||
| </script> | </script> | ||||||
| <div class="p-4 low-interaction flex gap-y-2 flex-col"> | <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> |     <SidebarUnit> | ||||||
| 
 | 
 | ||||||
|       <h3>Pick a filter below</h3> |       <h3>Pick a filter below</h3> | ||||||
| 
 | 
 | ||||||
|       <div class="flex flex-wrap"> |       <div class="flex flex-wrap"> | ||||||
|         {#each $filterResults as filterResult (filterResult)} |         {#each $filterResultsClipped as filterResult (filterResult)} | ||||||
|           <FilterResult {state} entry={filterResult} /> |           <FilterResultSvelte {state} entry={filterResult} /> | ||||||
|         {/each} |         {/each} | ||||||
|       </div> |       </div> | ||||||
|  |       {#if $filterResults.length + $layerResults.length > $filterResultsClipped.length} | ||||||
|  |         <div class="flex justify-center"> | ||||||
|  |           ... and {$filterResults.length + $layerResults.length - $filterResultsClipped.length} more ... | ||||||
|  |         </div> | ||||||
|  |       {/if} | ||||||
|     </SidebarUnit> |     </SidebarUnit> | ||||||
|   {/if} |   {/if} | ||||||
| 
 | 
 | ||||||
|  | @ -68,7 +96,7 @@ | ||||||
| 
 | 
 | ||||||
|       {:else if !$isSearching} |       {:else if !$isSearching} | ||||||
|         <b class="flex justify-center p-4"> |         <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> |         </b> | ||||||
|       {/if} |       {/if} | ||||||
|     </SidebarUnit> |     </SidebarUnit> | ||||||
|  | @ -88,8 +116,16 @@ | ||||||
|     </SidebarUnit> |     </SidebarUnit> | ||||||
|   {/if} |   {/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> |     <SidebarUnit> | ||||||
|       <div class="flex justify-between"> |       <div class="flex justify-between"> | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -326,7 +326,7 @@ | ||||||
| 
 | 
 | ||||||
|     <div |     <div | ||||||
|       id="top-bar" |       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 --> |       <!-- Top bar with tools --> | ||||||
|       <div class="flex items-center"> |       <div class="flex items-center"> | ||||||
| 
 | 
 | ||||||
|  | @ -361,7 +361,7 @@ | ||||||
|       {/if} |       {/if} | ||||||
| 
 | 
 | ||||||
|       <If condition={state.featureSwitches.featureSwitchSearch}> |       <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"> |           <div class="w-full sm:w-64"> | ||||||
|             <Searchbar value={state.searchState.searchTerm} isFocused={state.searchState.searchIsFocused} /> |             <Searchbar value={state.searchState.searchTerm} isFocused={state.searchState.searchIsFocused} /> | ||||||
|           </div> |           </div> | ||||||
|  |  | ||||||
|  | @ -391,8 +391,8 @@ h2.group { | ||||||
|     align-items: center; |     align-items: center; | ||||||
|     white-space: nowrap; |     white-space: nowrap; | ||||||
|     border-radius: 999rem; |     border-radius: 999rem; | ||||||
|     padding-left: 0.5rem; |     padding-left: 0.25rem; | ||||||
|     padding-right: 0.5rem; |     padding-right: 0.25rem; | ||||||
|     border: 1px solid var(--subtle-detail-color-light-contrast); |     border: 1px solid var(--subtle-detail-color-light-contrast); | ||||||
|     background-color: var(--low-interaction-background); |     background-color: var(--low-interaction-background); | ||||||
| } | } | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue