forked from MapComplete/MapComplete
		
	Improve search UI
This commit is contained in:
		
							parent
							
								
									3be286c2b1
								
							
						
					
					
						commit
						93f03ddbaf
					
				
					 22 changed files with 564 additions and 499 deletions
				
			
		|  | @ -537,7 +537,7 @@ | ||||||
|         }, |         }, | ||||||
|         { |         { | ||||||
|           "if": "cuisine=mexican  ", |           "if": "cuisine=mexican  ", | ||||||
|           "icon": "\uD83C\uDDF2\uD83C\uDDFD", |           "icon": "🇲🇽", | ||||||
|           "then": { |           "then": { | ||||||
|             "en": "Mexican dishes are served here", |             "en": "Mexican dishes are served here", | ||||||
|             "nl": "Dit is een mexicaans restaurant" |             "nl": "Dit is een mexicaans restaurant" | ||||||
|  |  | ||||||
|  | @ -1913,6 +1913,10 @@ input[type="range"].range-lg::-moz-range-thumb { | ||||||
|   max-height: 3rem; |   max-height: 3rem; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | .max-h-screen { | ||||||
|  |   max-height: 100vh; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| .max-h-full { | .max-h-full { | ||||||
|   max-height: 100%; |   max-height: 100%; | ||||||
| } | } | ||||||
|  | @ -1925,10 +1929,6 @@ input[type="range"].range-lg::-moz-range-thumb { | ||||||
|   max-height: 16rem; |   max-height: 16rem; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .max-h-96 { |  | ||||||
|   max-height: 24rem; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .max-h-60 { | .max-h-60 { | ||||||
|   max-height: 15rem; |   max-height: 15rem; | ||||||
| } | } | ||||||
|  | @ -8168,12 +8168,12 @@ svg.apply-fill path { | ||||||
|     width: 16rem; |     width: 16rem; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   .sm\:w-11 { |   .sm\:w-80 { | ||||||
|     width: 2.75rem; |     width: 20rem; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   .sm\:w-96 { |   .sm\:w-11 { | ||||||
|     width: 24rem; |     width: 2.75rem; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   .sm\:w-auto { |   .sm\:w-auto { | ||||||
|  | @ -8188,6 +8188,10 @@ svg.apply-fill path { | ||||||
|     width: 1.5rem; |     width: 1.5rem; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   .sm\:w-96 { | ||||||
|  |     width: 24rem; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|   .sm\:grid-cols-2 { |   .sm\:grid-cols-2 { | ||||||
|     grid-template-columns: repeat(2, minmax(0, 1fr)); |     grid-template-columns: repeat(2, minmax(0, 1fr)); | ||||||
|   } |   } | ||||||
|  | @ -8373,6 +8377,10 @@ svg.apply-fill path { | ||||||
|     height: 100%; |     height: 100%; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   .md\:w-96 { | ||||||
|  |     width: 24rem; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|   .md\:w-6\/12 { |   .md\:w-6\/12 { | ||||||
|     width: 50%; |     width: 50%; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  | @ -1,5 +1,5 @@ | ||||||
| import { ImmutableStore, Store } from "../UIEventSource" | import { ImmutableStore, Store } from "../UIEventSource" | ||||||
| import GeocodingProvider, { GeocodingOptions, SearchResult } from "./GeocodingProvider" | import GeocodingProvider, { FilterPayload, FilterResult, GeocodingOptions, SearchResult } from "./GeocodingProvider" | ||||||
| import { SpecialVisualizationState } from "../../UI/SpecialVisualization" | import { SpecialVisualizationState } from "../../UI/SpecialVisualization" | ||||||
| import { Utils } from "../../Utils" | import { Utils } from "../../Utils" | ||||||
| import Locale from "../../UI/i18n/Locale" | import Locale from "../../UI/i18n/Locale" | ||||||
|  | @ -13,17 +13,29 @@ export default class FilterSearch implements GeocodingProvider { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     async search(query: string): Promise<SearchResult[]> { |     async search(query: string): Promise<SearchResult[]> { | ||||||
|         return this.searchDirectly(query) |         return this.searchDirectlyWrapped(query) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private searchDirectly(query: string): SearchResult[] { |     private searchDirectlyWrapped(query: string): FilterResult[] { | ||||||
|         const possibleFilters: SearchResult[] = [] |         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 [] | ||||||
|         } |         } | ||||||
|         if(!Utils.isEmoji(query)){ |         const queries = query.split(" ").map(query => { | ||||||
|             query = Utils.simplifyStringForSearch(query) |             if (!Utils.isEmoji(query)) { | ||||||
|         } |                 return Utils.simplifyStringForSearch(query) | ||||||
|  |             } | ||||||
|  |             return query | ||||||
|  |         }).filter(q => q.length > 0) | ||||||
|  |         console.log("Queries:",queries) | ||||||
|  |         const possibleFilters: FilterPayload[] = [] | ||||||
|         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 | ||||||
|  | @ -34,22 +46,27 @@ export default class FilterSearch implements GeocodingProvider { | ||||||
|                     if (option === undefined) { |                     if (option === undefined) { | ||||||
|                         continue |                         continue | ||||||
|                     } |                     } | ||||||
|  |                     if (!option.osmTags) { | ||||||
|  |                         continue | ||||||
|  |                     } | ||||||
|                     let terms = ([option.question.txt, |                     let terms = ([option.question.txt, | ||||||
|                         ...(option.searchTerms?.[Locale.language.data] ?? option.searchTerms?.["en"] ?? [])] |                         ...(option.searchTerms?.[Locale.language.data] ?? option.searchTerms?.["en"] ?? [])] | ||||||
|                         .flatMap(term => [term, ...term?.split(" ")])) |                         .flatMap(term => [term, ...(term?.split(" ") ?? [])])) | ||||||
|                     terms = terms.map(t => Utils.simplifyStringForSearch(t)) |                     terms = terms.map(t => Utils.simplifyStringForSearch(t)) | ||||||
|                     terms.push(option.emoji) |                     terms.push(option.emoji) | ||||||
|                     Utils.NoNullInplace(terms) |                     Utils.NoNullInplace(terms) | ||||||
|                     const levehnsteinD = Math.min(... |                     const distances =    queries.flatMap(query => terms.map(entry => { | ||||||
|                         terms.map(entry => Utils.levenshteinDistance(query, entry.slice(0, query.length)))) |                         const d = Utils.levenshteinDistance(query, entry.slice(0, query.length)) | ||||||
|                     if (levehnsteinD / query.length > 0.25) { |                         console.log(query,"?  +",terms, "=",d) | ||||||
|  |                         const dRelative = d / query.length | ||||||
|  |                         return dRelative | ||||||
|  |                     })) | ||||||
|  | 
 | ||||||
|  |                     const levehnsteinD = Math.min(...distances) | ||||||
|  |                     if (levehnsteinD > 0.25) { | ||||||
|                         continue |                         continue | ||||||
|                     } |                     } | ||||||
|                     possibleFilters.push({ |                     possibleFilters.push({ option, layer, filter, index: i }) | ||||||
|                         payload: { option, layer, filter, index: i }, |  | ||||||
|                         category: "filter", |  | ||||||
|                         osm_id: layer.id + "/" + filter.id + "/" + option.osmTags?.asHumanString() ?? "none", |  | ||||||
|                     }) |  | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|  | @ -57,11 +74,7 @@ export default class FilterSearch implements GeocodingProvider { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     suggest(query: string, options?: GeocodingOptions): Store<SearchResult[]> { |     suggest(query: string, options?: GeocodingOptions): Store<SearchResult[]> { | ||||||
|         if (Utils.isEmoji(query)) { |         return new ImmutableStore(this.searchDirectlyWrapped(query)) | ||||||
|             return new ImmutableStore(this.searchDirectly(query)) |  | ||||||
|         } |  | ||||||
|         query = Utils.simplifyStringForSearch(query) |  | ||||||
|         return new ImmutableStore(this.searchDirectly(query)) |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -45,8 +45,9 @@ export type GeocodeResult =  { | ||||||
|     source?: string |     source?: string | ||||||
| } | } | ||||||
| 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 SearchResult = | export type SearchResult = | ||||||
|     | { category: "filter", osm_id: string, payload:  FilterPayload } |     | FilterResult | ||||||
|     | { category: "theme", osm_id: string, payload: MinimalLayoutInformation } |     | { category: "theme", osm_id: string, payload: MinimalLayoutInformation } | ||||||
|     | GeocodeResult |     | GeocodeResult | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -20,31 +20,33 @@ export default class ThemeSearch implements GeocodingProvider { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     async search(query: string): Promise<SearchResult[]> { |     async search(query: string): Promise<SearchResult[]> { | ||||||
|         return this.searchDirect(query, 99) |         return this.searchWrapped(query, 99) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     suggest(query: string, options?: GeocodingOptions): Store<SearchResult[]> { |     suggest(query: string, options?: GeocodingOptions): Store<SearchResult[]> { | ||||||
|         return new ImmutableStore(this.searchDirect(query, this._suggestionLimit ?? 4)) |         return new ImmutableStore(this.searchWrapped(query, this._suggestionLimit ?? 4)) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private searchDirect(query: string, limit: number): SearchResult[] { |  | ||||||
|         if(query.length < 1){ |  | ||||||
|             return [] |  | ||||||
|         } |  | ||||||
|         query = Utils.simplifyStringForSearch(query) |  | ||||||
|         const withMatch = ThemeSearch.allThemes |  | ||||||
|             .filter(th => !th.hideFromOverview || this._knownHiddenThemes.data.has(th.id)) |  | ||||||
|             .filter(th => th.id !== this._state.layout.id) |  | ||||||
|             .filter(th => MoreScreen.MatchesLayout(th, query)) |  | ||||||
|             .slice(0, limit) |  | ||||||
|         console.log("Matched", withMatch, limit) |  | ||||||
| 
 | 
 | ||||||
|         return withMatch.map(match => <SearchResult> { |     private searchWrapped(query: string, limit: number): SearchResult[] { | ||||||
|  |         return this.searchDirect(query, limit).map(match => <SearchResult>{ | ||||||
|             payload: match, |             payload: match, | ||||||
|             category: "theme", |             category: "theme", | ||||||
|             osm_id: match.id |             osm_id: match.id | ||||||
|         }) |         }) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     public searchDirect(query: string, limit: number): MinimalLayoutInformation[] { | ||||||
|  |         if (query.length < 1) { | ||||||
|  |             return [] | ||||||
|  |         } | ||||||
|  |         query = Utils.simplifyStringForSearch(query) | ||||||
|  |         return ThemeSearch.allThemes | ||||||
|  |             .filter(th => !th.hideFromOverview || this._knownHiddenThemes.data.has(th.id)) | ||||||
|  |             .filter(th => th.id !== this._state.layout.id) | ||||||
|  |             .filter(th => MoreScreen.MatchesLayout(th, query)) | ||||||
|  |             .slice(0, limit) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
| 
 | 
 | ||||||
| } | } | ||||||
|  |  | ||||||
							
								
								
									
										189
									
								
								src/Logic/State/SearchState.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										189
									
								
								src/Logic/State/SearchState.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,189 @@ | ||||||
|  | import GeocodingProvider, { FilterPayload, GeocodingUtils, type SearchResult } from "../Geocoding/GeocodingProvider" | ||||||
|  | import { RecentSearch } from "../Geocoding/RecentSearch" | ||||||
|  | import { 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 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 ShowDataLayer from "../../UI/Map/ShowDataLayer" | ||||||
|  | 
 | ||||||
|  | export default class SearchState { | ||||||
|  | 
 | ||||||
|  |     public readonly isSearching = new UIEventSource(false) | ||||||
|  |     public readonly geosearch: GeocodingProvider | ||||||
|  |     public readonly recentlySearched: RecentSearch | ||||||
|  |     public readonly feedback: UIEventSource<Translation> = new UIEventSource<Translation>(undefined) | ||||||
|  |     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 themeSuggestions: Store<MinimalLayoutInformation[]> | ||||||
|  | 
 | ||||||
|  |     private readonly state: ThemeViewState | ||||||
|  |     public readonly showSearchDrawer: UIEventSource<boolean> | ||||||
|  | 
 | ||||||
|  |     constructor(state: ThemeViewState) { | ||||||
|  |         this.state = state | ||||||
|  | 
 | ||||||
|  |         this.geosearch = new CombinedSearcher( | ||||||
|  |            // new LocalElementSearch(state, 5),
 | ||||||
|  |             new CoordinateSearch(), | ||||||
|  |             new OpenStreetMapIdSearch(state), | ||||||
|  |             new PhotonSearch() // new NominatimGeocoding(),
 | ||||||
|  |         ) | ||||||
|  | 
 | ||||||
|  |         this.recentlySearched = new RecentSearch(state) | ||||||
|  |         const bounds = state.mapProperties.bounds | ||||||
|  |         this.suggestions = this.searchTerm.stabilized(250).bindD(search => { | ||||||
|  |                 if (search.length === 0) { | ||||||
|  |                     return undefined | ||||||
|  |                 } | ||||||
|  |                 return Stores.holdDefined(bounds.bindD(bbox => this.geosearch.suggest(search, { bbox }))) | ||||||
|  |             } | ||||||
|  |         ) | ||||||
|  | 
 | ||||||
|  |         const themeSearch = new ThemeSearch(state, 3) | ||||||
|  |         this.themeSuggestions = this.searchTerm.mapD(query => themeSearch.searchDirect(query, 3)) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |         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) | ||||||
|  | 
 | ||||||
|  |                 return !foundMatch | ||||||
|  |             }) | ||||||
|  |         }, [state.layerState.activeFilters]) | ||||||
|  |         const geocodedFeatures = new GeocodingFeatureSource(this.suggestions.stabilized(250)) | ||||||
|  |         state.featureProperties.trackFeatureSource(geocodedFeatures) | ||||||
|  | 
 | ||||||
|  |         new ShowDataLayer( | ||||||
|  |             state.map, | ||||||
|  |             { | ||||||
|  |                 layer: GeocodingUtils.searchLayer, | ||||||
|  |                 features: geocodedFeatures, | ||||||
|  |                 selectedElement: state.selectedElement | ||||||
|  |             } | ||||||
|  |         ) | ||||||
|  | 
 | ||||||
|  |         this.showSearchDrawer = new UIEventSource(false) | ||||||
|  |         this.suggestions.addCallbackAndRunD(sugg => { | ||||||
|  |             if (sugg.length > 0) { | ||||||
|  |                 this.showSearchDrawer.set(true) | ||||||
|  |             } | ||||||
|  |         }) | ||||||
|  |         this.searchIsFocused.addCallbackAndRunD(sugg => { | ||||||
|  |             if (sugg) { | ||||||
|  |                 this.showSearchDrawer.set(true) | ||||||
|  |             } | ||||||
|  |         }) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |     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) | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         console.log("Could not apply", layer.id, ".", filter.id, index) | ||||||
|  |         if (filtercontrol.data === index) { | ||||||
|  |             filtercontrol.setData(undefined) | ||||||
|  |         } else { | ||||||
|  |             filtercontrol.setData(index) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * 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 searcher = this.state.searchState.geosearch | ||||||
|  |         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
 | ||||||
|  |             const result = await searcher.search(query, { bbox }) | ||||||
|  |             if (result.length == 0) { | ||||||
|  |                 this.feedback.set(Translations.t.general.search.nothing) | ||||||
|  |                 return false | ||||||
|  |             } | ||||||
|  |             const poi = result[0] | ||||||
|  |             if (poi.category === "theme") { | ||||||
|  |                 const theme = <MinimalLayoutInformation>poi.payload | ||||||
|  |                 const url = MoreScreen.createUrlFor(theme, false) | ||||||
|  |                 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) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | @ -238,8 +238,8 @@ export abstract class Store<T> implements Readable<T> { | ||||||
|      * src.setData(0) |      * src.setData(0) | ||||||
|      * lastValue // => "def"
 |      * lastValue // => "def"
 | ||||||
|      */ |      */ | ||||||
|     public bind<X>(f: (t: T) => Store<X>): Store<X> { |     public bind<X>(f: (t: T) => Store<X>, extraSources: Store<object>[] = []): Store<X> { | ||||||
|         const mapped = this.map(f) |         const mapped = this.map(f, extraSources) | ||||||
|         const sink = new UIEventSource<X>(undefined) |         const sink = new UIEventSource<X>(undefined) | ||||||
|         const seenEventSources = new Set<Store<X>>() |         const seenEventSources = new Set<Store<X>>() | ||||||
|         mapped.addCallbackAndRun((newEventSource) => { |         mapped.addCallbackAndRun((newEventSource) => { | ||||||
|  | @ -270,7 +270,7 @@ export abstract class Store<T> implements Readable<T> { | ||||||
|         return sink |         return sink | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public bindD<X>(f: (t: Exclude<T, undefined | null>) => Store<X>): Store<X> { |     public bindD<X>(f: (t: Exclude<T, undefined | null>) => Store<X>, extraSources: UIEventSource<object>[] =[]): Store<X> { | ||||||
|         return this.bind((t) => { |         return this.bind((t) => { | ||||||
|             if (t === null) { |             if (t === null) { | ||||||
|                 return null |                 return null | ||||||
|  | @ -279,7 +279,7 @@ export abstract class Store<T> implements Readable<T> { | ||||||
|                 return undefined |                 return undefined | ||||||
|             } |             } | ||||||
|             return f(<Exclude<T, undefined | null>>t) |             return f(<Exclude<T, undefined | null>>t) | ||||||
|         }) |         }, extraSources) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public stabilized(millisToStabilize): Store<T> { |     public stabilized(millisToStabilize): Store<T> { | ||||||
|  |  | ||||||
|  | @ -340,7 +340,7 @@ export default class LayoutConfig implements LayoutInformation { | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|         console.trace("Fallthrough: could not find the appropraite layer for an object with tags", tags, "within layout", this) |         console.trace("Fallthrough: could not find the appropriate layer for an object with tags", tags, "within layout", this) | ||||||
|         return undefined |         return undefined | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -83,6 +83,7 @@ import PhotonSearch from "../Logic/Geocoding/PhotonSearch" | ||||||
| import ThemeSearch from "../Logic/Geocoding/ThemeSearch" | import ThemeSearch from "../Logic/Geocoding/ThemeSearch" | ||||||
| import OpenStreetMapIdSearch from "../Logic/Geocoding/OpenStreetMapIdSearch" | import OpenStreetMapIdSearch from "../Logic/Geocoding/OpenStreetMapIdSearch" | ||||||
| import FilterSearch from "../Logic/Geocoding/FilterSearch" | import FilterSearch from "../Logic/Geocoding/FilterSearch" | ||||||
|  | import SearchState from "../Logic/State/SearchState" | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * |  * | ||||||
|  | @ -164,8 +165,7 @@ export default class ThemeViewState implements SpecialVisualizationState { | ||||||
| 
 | 
 | ||||||
|     public readonly nearbyImageSearcher: CombinedFetcher |     public readonly nearbyImageSearcher: CombinedFetcher | ||||||
| 
 | 
 | ||||||
|     public readonly geosearch: GeocodingProvider |     public readonly searchState: SearchState | ||||||
|     public readonly recentlySearched: RecentSearch |  | ||||||
| 
 | 
 | ||||||
|     constructor(layout: LayoutConfig, mvtAvailableLayers: Set<string>) { |     constructor(layout: LayoutConfig, mvtAvailableLayers: Set<string>) { | ||||||
|         Utils.initDomPurify() |         Utils.initDomPurify() | ||||||
|  | @ -390,16 +390,7 @@ export default class ThemeViewState implements SpecialVisualizationState { | ||||||
|         this.featureSummary = this.setupSummaryLayer() |         this.featureSummary = this.setupSummaryLayer() | ||||||
|         this.toCacheSavers = layout.enableCache ? this.initSaveToLocalStorage() : undefined |         this.toCacheSavers = layout.enableCache ? this.initSaveToLocalStorage() : undefined | ||||||
| 
 | 
 | ||||||
|         this.geosearch = new CombinedSearcher( |         this.searchState = new SearchState(this) | ||||||
|             new FilterSearch(this), |  | ||||||
|             new LocalElementSearch(this, 5), |  | ||||||
|             new CoordinateSearch(), |  | ||||||
|             this.featureSwitches.featureSwitchBackToThemeOverview.data ? new ThemeSearch(this, 3) : undefined, |  | ||||||
|             new OpenStreetMapIdSearch(this), |  | ||||||
|             new PhotonSearch(), // new NominatimGeocoding(),
 |  | ||||||
|         ) |  | ||||||
| 
 |  | ||||||
|         this.recentlySearched = new RecentSearch(this) |  | ||||||
| 
 | 
 | ||||||
|         this.initActors() |         this.initActors() | ||||||
|         this.drawSpecialLayers() |         this.drawSpecialLayers() | ||||||
|  | @ -931,7 +922,6 @@ export default class ThemeViewState implements SpecialVisualizationState { | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Searches the appropriate layer - will first try if a special layer matches; if not, a normal layer will be used by delegating to the layout |      * Searches the appropriate layer - will first try if a special layer matches; if not, a normal layer will be used by delegating to the layout | ||||||
|      * @param tags |  | ||||||
|      */ |      */ | ||||||
|     public getMatchingLayer(properties: Record<string, string>){ |     public getMatchingLayer(properties: Record<string, string>){ | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
							
								
								
									
										43
									
								
								src/UI/Base/DrawerRight.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								src/UI/Base/DrawerRight.svelte
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,43 @@ | ||||||
|  | <script lang="ts"> | ||||||
|  |   import { Drawer } from "flowbite-svelte" | ||||||
|  |   import { sineIn } from "svelte/easing" | ||||||
|  |   import { Store } from "../../Logic/UIEventSource.js" | ||||||
|  |   import { onMount } from "svelte" | ||||||
|  | 
 | ||||||
|  |   export let shown: Store<boolean> | ||||||
|  |   let transitionParams = { | ||||||
|  |     x: 640, | ||||||
|  |     duration: 200, | ||||||
|  |     easing: sineIn | ||||||
|  |   } | ||||||
|  |   let hidden = !shown.data | ||||||
|  | 
 | ||||||
|  |   shown.addCallback(sh => { | ||||||
|  |     hidden = !sh | ||||||
|  |   }) | ||||||
|  | 
 | ||||||
|  |   let height = 0 | ||||||
|  |   onMount(() => { | ||||||
|  |     let topbar = document.getElementById("top-bar") | ||||||
|  |     height = topbar.clientHeight | ||||||
|  |   }) | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <Drawer placement="right" | ||||||
|  |         transitionType="fly" {transitionParams} | ||||||
|  |         activateClickOutside={false} | ||||||
|  |         divClass="overflow-y-auto" | ||||||
|  |         backdrop={false} | ||||||
|  |         id="drawer-right" | ||||||
|  |         width="w-full sm:w-80 md:w-96" | ||||||
|  |         rightOffset="inset-y-0 right-0" | ||||||
|  |         bind:hidden={hidden}> | ||||||
|  | 
 | ||||||
|  |   <div class="normal-background h-screen"> | ||||||
|  |     <div class="h-full" style={`padding-top: ${height}px`}> | ||||||
|  |       <div class="flex flex-col h-full overflow-y-auto"> | ||||||
|  |         <slot /> | ||||||
|  |       </div> | ||||||
|  |     </div> | ||||||
|  |   </div> | ||||||
|  | </Drawer> | ||||||
|  | @ -2,17 +2,12 @@ | ||||||
|   import Translations from "../i18n/Translations" |   import Translations from "../i18n/Translations" | ||||||
|   import Tr from "../Base/Tr.svelte" |   import Tr from "../Base/Tr.svelte" | ||||||
|   import NextButton from "../Base/NextButton.svelte" |   import NextButton from "../Base/NextButton.svelte" | ||||||
|   import Geosearch from "../Search/Geosearch.svelte" |  | ||||||
|   import ThemeViewState from "../../Models/ThemeViewState" |   import ThemeViewState from "../../Models/ThemeViewState" | ||||||
|   import { Store, UIEventSource } from "../../Logic/UIEventSource" |   import { Store } from "../../Logic/UIEventSource" | ||||||
|   import { SearchIcon } from "@rgossiaux/svelte-heroicons/solid" |  | ||||||
|   import { twJoin } from "tailwind-merge" |  | ||||||
|   import { Utils } from "../../Utils" |  | ||||||
|   import type { GeolocationPermissionState } from "../../Logic/State/GeoLocationState" |   import type { GeolocationPermissionState } from "../../Logic/State/GeoLocationState" | ||||||
|   import { GeoLocationState } from "../../Logic/State/GeoLocationState" |   import { GeoLocationState } from "../../Logic/State/GeoLocationState" | ||||||
|   import If from "../Base/If.svelte" |   import If from "../Base/If.svelte" | ||||||
|   import { ExclamationTriangleIcon } from "@babeard/svelte-heroicons/mini" |   import { ExclamationTriangleIcon } from "@babeard/svelte-heroicons/mini" | ||||||
|   import ChevronDoubleLeft from "@babeard/svelte-heroicons/solid/ChevronDoubleLeft" |  | ||||||
|   import GeolocationIndicator from "./GeolocationIndicator.svelte" |   import GeolocationIndicator from "./GeolocationIndicator.svelte" | ||||||
| 
 | 
 | ||||||
|   /** |   /** | ||||||
|  | @ -20,10 +15,6 @@ | ||||||
|    */ |    */ | ||||||
|   export let state: ThemeViewState |   export let state: ThemeViewState | ||||||
|   let layout = state.layout |   let layout = state.layout | ||||||
|   let selectedElement = state.selectedElement |  | ||||||
| 
 |  | ||||||
|   let triggerSearch: UIEventSource<any> = new UIEventSource<any>(undefined) |  | ||||||
|   let searchEnabled = false |  | ||||||
| 
 | 
 | ||||||
|   let geolocation = state.geolocation.geolocationState |   let geolocation = state.geolocation.geolocationState | ||||||
|   let geopermission: Store<GeolocationPermissionState> = geolocation.permission |   let geopermission: Store<GeolocationPermissionState> = geolocation.permission | ||||||
|  | @ -35,7 +26,7 @@ | ||||||
|     state.geolocationControl.handleClick() |     state.geolocationControl.handleClick() | ||||||
|     const glstate = state.geolocation.geolocationState |     const glstate = state.geolocation.geolocationState | ||||||
|     if (glstate.currentGPSLocation.data !== undefined) { |     if (glstate.currentGPSLocation.data !== undefined) { | ||||||
|       const c: GeolocationCoordinates = glstate.currentGPSLocation.data |       const c = glstate.currentGPSLocation.data | ||||||
|       state.guistate.pageStates.about_theme.setData(false) |       state.guistate.pageStates.about_theme.setData(false) | ||||||
|       const coor = { lon: c.longitude, lat: c.latitude } |       const coor = { lon: c.longitude, lat: c.latitude } | ||||||
|       state.mapProperties.location.setData(coor) |       state.mapProperties.location.setData(coor) | ||||||
|  | @ -86,38 +77,6 @@ | ||||||
|           <Tr t={$gpsExplanation} /> |           <Tr t={$gpsExplanation} /> | ||||||
|         </button> |         </button> | ||||||
|       </If> |       </If> | ||||||
| 
 |  | ||||||
|       <If condition={state.featureSwitches.featureSwitchSearch}> |  | ||||||
|         <div |  | ||||||
|           class=".button low-interaction m-1 flex h-fit w-full flex-wrap items-center justify-end gap-x-2 gap-y-2 rounded border p-1" |  | ||||||
|         > |  | ||||||
|           <div style="min-width: 16rem; " class="grow"> |  | ||||||
|             <Geosearch |  | ||||||
|               bounds={state.mapProperties.bounds} |  | ||||||
|               on:searchCompleted={() => state.guistate.pageStates.about_theme.setData(false)} |  | ||||||
|               on:searchIsValid={(event) => { |  | ||||||
|                 searchEnabled = event.detail |  | ||||||
|               }} |  | ||||||
|               perLayer={state.perLayer} |  | ||||||
|               {selectedElement} |  | ||||||
|               {triggerSearch} |  | ||||||
|               geolocationState={state.geolocation.geolocationState} |  | ||||||
|               searcher={state.geosearch} |  | ||||||
|               {state} |  | ||||||
|             /> |  | ||||||
|           </div> |  | ||||||
|           <button |  | ||||||
|             class={twJoin( |  | ||||||
|               "small flex w-fit shrink-0 items-center justify-between gap-x-2", |  | ||||||
|               !searchEnabled && "disabled" |  | ||||||
|             )} |  | ||||||
|             on:click={() => triggerSearch.ping()} |  | ||||||
|           > |  | ||||||
|             <Tr t={Translations.t.general.search.searchShort} /> |  | ||||||
|             <SearchIcon class="h-6 w-6" /> |  | ||||||
|           </button> |  | ||||||
|         </div> |  | ||||||
|       </If> |  | ||||||
|     </div> |     </div> | ||||||
| 
 | 
 | ||||||
|     {#if $currentGPSLocation === undefined && $geopermission === "requested" && GeoLocationState.isSafari()} |     {#if $currentGPSLocation === undefined && $geopermission === "requested" && GeoLocationState.isSafari()} | ||||||
|  |  | ||||||
|  | @ -104,8 +104,7 @@ | ||||||
|             </div> |             </div> | ||||||
| 
 | 
 | ||||||
|             {#if $reason.includeSearch} |             {#if $reason.includeSearch} | ||||||
|               searcher={state.geosearch} |               <Geosearch {state}/> | ||||||
|               <Geosearch bounds={currentMapProperties.bounds} clearAfterView={false} searcher={state.geosearch} {state}/> |  | ||||||
|             {/if} |             {/if} | ||||||
| 
 | 
 | ||||||
|             <div class="flex flex-wrap"> |             <div class="flex flex-wrap"> | ||||||
|  |  | ||||||
|  | @ -1,20 +1,30 @@ | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
|   import type { ActiveFilter } from "../../Logic/State/LayerState" |   import type { ActiveFilter } from "../../Logic/State/LayerState" | ||||||
|   import Tr from "../Base/Tr.svelte" |  | ||||||
|   import Icon from "../Map/Icon.svelte" |  | ||||||
|   import { Badge } from "flowbite-svelte" |  | ||||||
|   import FilterOption from "./FilterOption.svelte" |   import FilterOption from "./FilterOption.svelte" | ||||||
|   import { XMarkIcon } from "@babeard/svelte-heroicons/mini" |   import { XMarkIcon } from "@babeard/svelte-heroicons/mini" | ||||||
|  |   import Loading from "../Base/Loading.svelte" | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|   export let activeFilter: ActiveFilter |   export let activeFilter: ActiveFilter | ||||||
|   let { control, layer, filter } = activeFilter |   let { control, filter } = activeFilter | ||||||
|   let option = control.map(c => filter.options[c] ?? filter.options[0]) |   let option = control.map(c => filter.options[c] ?? filter.options[0]) | ||||||
| </script> |   let loading = false | ||||||
| <div class="badge"> |  | ||||||
|   <FilterOption option={$option} /> |  | ||||||
|   <button on:click={() => control.setData(undefined)}> |  | ||||||
| 
 | 
 | ||||||
|     <XMarkIcon class="w-5 h-5 pl-1" color="gray" /> |   function clear() { | ||||||
|   </button> |     loading = true | ||||||
| </div> |     requestIdleCallback(() => { | ||||||
|  |       control.setData(undefined) | ||||||
|  |       loading = false | ||||||
|  |     }) | ||||||
|  |   } | ||||||
|  | </script> | ||||||
|  | {#if loading} | ||||||
|  |   <Loading /> | ||||||
|  | {:else } | ||||||
|  |   <div class="badge"> | ||||||
|  |     <FilterOption option={$option} /> | ||||||
|  |     <button on:click={() => clear()}> | ||||||
|  |       <XMarkIcon class="w-5 h-5 pl-1" color="gray" /> | ||||||
|  |     </button> | ||||||
|  |   </div> | ||||||
|  | {/if} | ||||||
|  |  | ||||||
|  | @ -1,24 +1,37 @@ | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
|   import { default as ActiveFilterSvelte } from "./ActiveFilter.svelte" |   import { default as ActiveFilterSvelte } from "./ActiveFilter.svelte" | ||||||
|   import type { ActiveFilter } from "../../Logic/State/LayerState" |   import type { ActiveFilter } from "../../Logic/State/LayerState" | ||||||
|  |   import Loading from "../Base/Loading.svelte" | ||||||
| 
 | 
 | ||||||
|   export let activeFilters: ActiveFilter[] |   export let activeFilters: ActiveFilter[] | ||||||
|  |   let loading = false | ||||||
| 
 | 
 | ||||||
|   function clear() { |   function clear() { | ||||||
|     for (const activeFilter of activeFilters) { |     loading = true | ||||||
|       activeFilter.control.setData(undefined) |     requestIdleCallback(() => { | ||||||
|     } | 
 | ||||||
|  |       for (const activeFilter of activeFilters) { | ||||||
|  |         activeFilter.control.setData(undefined) | ||||||
|  |       } | ||||||
|  |       loading = false | ||||||
|  |     }) | ||||||
|   } |   } | ||||||
| </script> | </script> | ||||||
| {#if activeFilters.length > 0} | {#if activeFilters.length > 0} | ||||||
|   <div class="flex flex-wrap gap-y-1 gap-x-1 button-unstyled"> |   <div class="flex flex-wrap gap-y-1 gap-x-1 button-unstyled"> | ||||||
|     {#each activeFilters as activeFilter (activeFilter)} |     <h3>Active filters</h3> | ||||||
|       <ActiveFilterSvelte {activeFilter} /> |  | ||||||
|     {/each} |  | ||||||
| 
 | 
 | ||||||
|     <button class="as-link subtle" on:click={() => clear()}> |     {#if loading} | ||||||
|       Clear filters |       <Loading /> | ||||||
|     </button> |     {:else} | ||||||
|  |       {#each activeFilters as activeFilter (activeFilter)} | ||||||
|  |         <ActiveFilterSvelte {activeFilter} /> | ||||||
|  |       {/each} | ||||||
|  | 
 | ||||||
|  |       <button class="as-link subtle" on:click={() => clear()}> | ||||||
|  |         Clear filters | ||||||
|  |       </button> | ||||||
|  |     {/if} | ||||||
|   </div> |   </div> | ||||||
| 
 | 
 | ||||||
| {/if} | {/if} | ||||||
|  |  | ||||||
|  | @ -4,19 +4,15 @@ | ||||||
|   import type { FilterPayload } from "../../Logic/Geocoding/GeocodingProvider" |   import type { FilterPayload } from "../../Logic/Geocoding/GeocodingProvider" | ||||||
|   import { createEventDispatcher } from "svelte" |   import { createEventDispatcher } from "svelte" | ||||||
|   import Icon from "../Map/Icon.svelte" |   import Icon from "../Map/Icon.svelte" | ||||||
|   import SearchResultUtils from "./SearchResultUtils" |  | ||||||
| 
 | 
 | ||||||
|   export let entry: { |   export let entry: FilterPayload | ||||||
|     category: "filter", |   let { option } = entry | ||||||
|     payload: FilterPayload |  | ||||||
|   } |  | ||||||
|   let { option, filter, layer, index } = entry.payload |  | ||||||
|   export let state: SpecialVisualizationState |   export let state: SpecialVisualizationState | ||||||
|   let dispatch = createEventDispatcher<{ select }>() |   let dispatch = createEventDispatcher<{ select }>() | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|   function apply() { |   function apply() { | ||||||
|     SearchResultUtils.apply(entry.payload, state) |     state.searchState.apply(entry) | ||||||
|     dispatch("select") |     dispatch("select") | ||||||
|   } |   } | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
|  | @ -1,53 +1,28 @@ | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
|   import { Store, Stores, UIEventSource } from "../../Logic/UIEventSource" |   import { UIEventSource } from "../../Logic/UIEventSource" | ||||||
|   import type { Feature } from "geojson" |  | ||||||
|   import Translations from "../i18n/Translations" |   import Translations from "../i18n/Translations" | ||||||
|   import Loading from "../Base/Loading.svelte" |   import Loading from "../Base/Loading.svelte" | ||||||
|   import Hotkeys from "../Base/Hotkeys" |   import Hotkeys from "../Base/Hotkeys" | ||||||
|   import { BBox } from "../../Logic/BBox" |  | ||||||
|   import { GeoIndexedStoreForLayer } from "../../Logic/FeatureSource/Actors/GeoIndexedStore" |  | ||||||
|   import { createEventDispatcher, onDestroy } from "svelte" |   import { createEventDispatcher, onDestroy } from "svelte" | ||||||
|   import { placeholder } from "../../Utils/placeholder" |   import { placeholder } from "../../Utils/placeholder" | ||||||
|   import { SearchIcon } from "@rgossiaux/svelte-heroicons/solid" |   import { SearchIcon } from "@rgossiaux/svelte-heroicons/solid" | ||||||
|   import { ariaLabel } from "../../Utils/ariaLabel" |   import { ariaLabel } from "../../Utils/ariaLabel" | ||||||
|   import { GeoLocationState } from "../../Logic/State/GeoLocationState" |  | ||||||
|   import { NominatimGeocoding } from "../../Logic/Geocoding/NominatimGeocoding" |  | ||||||
|   import { GeocodingUtils } from "../../Logic/Geocoding/GeocodingProvider" |  | ||||||
|   import type { SearchResult } from "../../Logic/Geocoding/GeocodingProvider" |  | ||||||
|   import type GeocodingProvider from "../../Logic/Geocoding/GeocodingProvider" |  | ||||||
| 
 | 
 | ||||||
|   import SearchResults from "./SearchResults.svelte" |  | ||||||
|   import type { MinimalLayoutInformation } from "../../Models/ThemeConfig/LayoutConfig" |  | ||||||
|   import { focusWithArrows } from "../../Utils/focusWithArrows" |   import { focusWithArrows } from "../../Utils/focusWithArrows" | ||||||
|   import ShowDataLayer from "../Map/ShowDataLayer" |  | ||||||
|   import ThemeViewState from "../../Models/ThemeViewState" |   import ThemeViewState from "../../Models/ThemeViewState" | ||||||
|   import GeocodingFeatureSource from "../../Logic/Geocoding/GeocodingFeatureSource" |  | ||||||
|   import MoreScreen from "../BigComponents/MoreScreen" |  | ||||||
|   import SearchResultUtils from "./SearchResultUtils" |  | ||||||
| 
 | 
 | ||||||
|   export let perLayer: ReadonlyMap<string, GeoIndexedStoreForLayer> | undefined = undefined |  | ||||||
|   export let bounds: UIEventSource<BBox> |  | ||||||
|   export let selectedElement: UIEventSource<Feature> | undefined = undefined |  | ||||||
| 
 |  | ||||||
|   export let geolocationState: GeoLocationState | undefined = undefined |  | ||||||
|   export let clearAfterView: boolean = true |  | ||||||
|   export let searcher: GeocodingProvider = new NominatimGeocoding() |  | ||||||
|   export let state: ThemeViewState |   export let state: ThemeViewState | ||||||
|   let searchContents: UIEventSource<string> = new UIEventSource<string>("") |   export let searchContents: UIEventSource<string> = new UIEventSource<string>("") | ||||||
|   export let triggerSearch: UIEventSource<any> = new UIEventSource<any>(undefined) |  | ||||||
|   onDestroy( |  | ||||||
|     triggerSearch.addCallback(() => { |  | ||||||
|       performSearch() |  | ||||||
|     }) |  | ||||||
|   ) |  | ||||||
| 
 | 
 | ||||||
|   let isRunning: boolean = false |   function performSearch() { | ||||||
|  |     state.searchState.performSearch() | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   let isRunning = state.searchState.isSearching | ||||||
| 
 | 
 | ||||||
|   let inputElement: HTMLInputElement |   let inputElement: HTMLInputElement | ||||||
| 
 | 
 | ||||||
|   let feedback: string = undefined |   export let isFocused = new UIEventSource(false) | ||||||
| 
 |  | ||||||
|   let isFocused = new UIEventSource(false) |  | ||||||
| 
 | 
 | ||||||
|   function focusOnSearch() { |   function focusOnSearch() { | ||||||
|     requestAnimationFrame(() => { |     requestAnimationFrame(() => { | ||||||
|  | @ -57,7 +32,7 @@ | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   Hotkeys.RegisterHotkey({ ctrl: "F" }, Translations.t.hotkeyDocumentation.selectSearch, () => { |   Hotkeys.RegisterHotkey({ ctrl: "F" }, Translations.t.hotkeyDocumentation.selectSearch, () => { | ||||||
|     feedback = undefined |     state.searchState.feedback.set(undefined) | ||||||
|     focusOnSearch() |     focusOnSearch() | ||||||
|   }) |   }) | ||||||
| 
 | 
 | ||||||
|  | @ -70,95 +45,6 @@ | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
|   async function performSearch() { |  | ||||||
|     try { |  | ||||||
|       isRunning = 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 |  | ||||||
|       const searchContentsData = $searchContents?.trim() ?? "" |  | ||||||
| 
 |  | ||||||
|       if (searchContentsData === "") { |  | ||||||
|         return |  | ||||||
|       } |  | ||||||
|       const result = await searcher.search(searchContentsData, { bbox: bounds.data, limit: 10 }) |  | ||||||
|       if (result.length == 0) { |  | ||||||
|         feedback = Translations.t.general.search.nothing.txt |  | ||||||
|         focusOnSearch() |  | ||||||
|         return |  | ||||||
|       } |  | ||||||
|       const poi = result[0] |  | ||||||
|       if (poi.category === "theme") { |  | ||||||
|         const theme = <MinimalLayoutInformation>poi.payload |  | ||||||
|         const url = MoreScreen.createUrlFor(theme, false) |  | ||||||
|         // @ts-ignore |  | ||||||
|         window.location = url |  | ||||||
|         return |  | ||||||
|       } |  | ||||||
|       if(poi.category === "filter"){ |  | ||||||
|         SearchResultUtils.apply(poi.payload, state) |  | ||||||
|       } |  | ||||||
|       if(poi.category === "filter"){ |  | ||||||
|         return  // Should not happen |  | ||||||
|       } |  | ||||||
|       if (poi.boundingbox) { |  | ||||||
| 
 |  | ||||||
|         const [lat0, lat1, lon0, lon1] = poi.boundingbox |  | ||||||
|         bounds.set( |  | ||||||
|           new BBox([ |  | ||||||
|             [lon0, lat0], |  | ||||||
|             [lon1, lat1] |  | ||||||
|           ]).pad(0.01) |  | ||||||
|         ) |  | ||||||
|       } else if (poi.lon && poi.lat) { |  | ||||||
|         state.mapProperties.flyTo(poi.lon, poi.lat, GeocodingUtils.categoryToZoomLevel[poi.category] ?? 16) |  | ||||||
|       } |  | ||||||
|       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 |  | ||||||
|           } |  | ||||||
|           selectedElement?.setData(found) |  | ||||||
|           break |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|       if (clearAfterView) { |  | ||||||
|         searchContents.setData("") |  | ||||||
|       } |  | ||||||
|       dispatch("searchIsValid", false) |  | ||||||
|       dispatch("searchCompleted") |  | ||||||
|       isFocused.setData(false) |  | ||||||
|     } catch (e) { |  | ||||||
|       console.error(e) |  | ||||||
|       feedback = Translations.t.general.search.error.txt |  | ||||||
|       focusOnSearch() |  | ||||||
|     } finally { |  | ||||||
|       isRunning = false |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   let suggestions: Store<SearchResult[]> = searchContents.stabilized(250).bindD(search => { |  | ||||||
|       if (search.length === 0) { |  | ||||||
|         return undefined |  | ||||||
|       } |  | ||||||
|       return Stores.holdDefined(bounds.bindD(bbox => searcher.suggest(search, { bbox, limit: 15 }))) |  | ||||||
|     } |  | ||||||
|   ) |  | ||||||
|   let geocededFeatures=  new GeocodingFeatureSource(suggestions.stabilized(250)) |  | ||||||
|   state.featureProperties.trackFeatureSource(geocededFeatures) |  | ||||||
| 
 |  | ||||||
|   new ShowDataLayer( |  | ||||||
|     state.map, |  | ||||||
|     { |  | ||||||
|       layer: GeocodingUtils.searchLayer, |  | ||||||
|       features:  geocededFeatures, |  | ||||||
|       selectedElement: state.selectedElement |  | ||||||
|     } |  | ||||||
|   ) |  | ||||||
| 
 |  | ||||||
|   let geosearch: HTMLDivElement |   let geosearch: HTMLDivElement | ||||||
| 
 | 
 | ||||||
|   function checkFocus() { |   function checkFocus() { | ||||||
|  | @ -181,7 +67,7 @@ | ||||||
| 
 | 
 | ||||||
|   <div class="normal-background flex justify-between rounded-full pl-2 w-full"> |   <div class="normal-background flex justify-between rounded-full pl-2 w-full"> | ||||||
|     <form class="flex w-full flex-wrap"> |     <form class="flex w-full flex-wrap"> | ||||||
|       {#if isRunning} |       {#if $isRunning} | ||||||
|         <Loading>{Translations.t.general.search.searching}</Loading> |         <Loading>{Translations.t.general.search.searching}</Loading> | ||||||
|       {:else} |       {:else} | ||||||
|         <input |         <input | ||||||
|  | @ -189,12 +75,11 @@ | ||||||
|           class="w-full outline-none" |           class="w-full outline-none" | ||||||
|           bind:this={inputElement} |           bind:this={inputElement} | ||||||
|           on:keypress={(keypr) => { |           on:keypress={(keypr) => { | ||||||
|           feedback = undefined |             if(keypr.key === "Enter"){ | ||||||
|           if(keypr.key === "Enter"){ |               performSearch() | ||||||
|             performSearch() |               keypr.preventDefault() | ||||||
|             keypr.preventDefault() |             } | ||||||
|           } |             return undefined | ||||||
|           return undefined |  | ||||||
|         }} |         }} | ||||||
|           on:focus={() => {isFocused.setData(true)}} |           on:focus={() => {isFocused.setData(true)}} | ||||||
|           on:blur={() => {checkFocus()}} |           on:blur={() => {checkFocus()}} | ||||||
|  | @ -202,21 +87,9 @@ | ||||||
|           use:placeholder={Translations.t.general.search.search} |           use:placeholder={Translations.t.general.search.search} | ||||||
|           use:ariaLabel={Translations.t.general.search.search} |           use:ariaLabel={Translations.t.general.search.search} | ||||||
|         /> |         /> | ||||||
|         {#if feedback !== undefined} | 
 | ||||||
|           <!-- The feedback is _always_ shown for screenreaders and to make sure that the searchfield can still be selected by tabbing--> |  | ||||||
|           <div class="alert" role="alert" aria-live="assertive"> |  | ||||||
|             {feedback} |  | ||||||
|           </div> |  | ||||||
|         {/if} |  | ||||||
|       {/if} |       {/if} | ||||||
|     </form> |     </form> | ||||||
|     <SearchIcon aria-hidden="true" class="h-6 w-6 self-end" on:click={performSearch} /> |     <SearchIcon aria-hidden="true" class="h-6 w-6 self-end" on:click={() => performSearch()} /> | ||||||
|   </div> |  | ||||||
| 
 |  | ||||||
|   <div class="relative h-0" style="z-index: 10"> |  | ||||||
|     <div class="absolute right-0 w-full sm:w-96 h-fit max-h-96"> |  | ||||||
|       <SearchResults {isFocused} {state} results={$suggestions} searchTerm={searchContents} |  | ||||||
|                      on:select={() => {searchContents.set(""); isFocused.setData(false)}} /> |  | ||||||
|     </div> |  | ||||||
|   </div> |   </div> | ||||||
| </div> | </div> | ||||||
|  |  | ||||||
|  | @ -11,9 +11,9 @@ | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| {#if entry.category === "theme"} | {#if entry.category === "theme"} | ||||||
|   <ThemeResult {entry} on:select  /> |   <ThemeResult entry={entry.payload} on:select  /> | ||||||
| {:else if entry.category === "filter"} | {:else if entry.category === "filter"} | ||||||
|   <FilterResult {entry} {state} on:select /> |   <FilterResult entry={entry.payload} {state} on:select /> | ||||||
| {:else} | {:else} | ||||||
|   <GeocodeResult {entry} {state} on:select /> |   <GeocodeResult {entry} {state} on:select /> | ||||||
| {/if} | {/if} | ||||||
|  |  | ||||||
|  | @ -1,25 +0,0 @@ | ||||||
| import { SpecialVisualizationState } from "../SpecialVisualization" |  | ||||||
| import { FilterPayload } from "../../Logic/Geocoding/GeocodingProvider" |  | ||||||
| 
 |  | ||||||
| export default class SearchResultUtils { |  | ||||||
|     static apply(payload: FilterPayload, state: SpecialVisualizationState) { |  | ||||||
|         const { layer, filter, index, option } = payload |  | ||||||
| 
 |  | ||||||
|         let flayer = state.layerState.filteredLayers.get(layer.id) |  | ||||||
|         let filtercontrol = flayer.appliedFilters.get(filter.id) |  | ||||||
| 
 |  | ||||||
|         for (const [name, otherLayer] of state.layerState.filteredLayers) { |  | ||||||
|             if (name === layer.id) { |  | ||||||
|                 otherLayer.isDisplayed.setData(true) |  | ||||||
|                 continue |  | ||||||
|             } |  | ||||||
|             otherLayer.isDisplayed.setData(false) |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         if (filtercontrol.data === index) { |  | ||||||
|             filtercontrol.setData(undefined) |  | ||||||
|         } else { |  | ||||||
|             filtercontrol.setData(index) |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  | @ -1,104 +1,99 @@ | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
|   import type { SpecialVisualizationState } from "../SpecialVisualization" |   import { Store } from "../../Logic/UIEventSource" | ||||||
|   import { Store, UIEventSource } 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, SearchResult } from "../../Logic/Geocoding/GeocodingProvider" |   import type { GeocodeResult } from "../../Logic/Geocoding/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 FilterResult from "./FilterResult.svelte" | ||||||
|  |   import ThemeResult from "./ThemeResult.svelte" | ||||||
| 
 | 
 | ||||||
|   export let state: SpecialVisualizationState |   export let state: ThemeViewState | ||||||
|   export let results: SearchResult[] |  | ||||||
|   export let searchTerm: Store<string> |  | ||||||
|   export let isFocused: UIEventSource<boolean> |  | ||||||
|   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.searchState.recentlySearched.seenThisSession | ||||||
|   let hasActiveFilters = activeFilters.map(afs => afs.length > 0) |  | ||||||
| 
 |  | ||||||
|   let recentlySeen: Store<GeocodeResult[]> = state.recentlySearched.seenThisSession |  | ||||||
|   let recentThemes = state.userRelatedState.recentlyVisitedThemes.mapD(thms => thms.filter(th => th !== state.layout.id).slice(0, 3)) |   let recentThemes = state.userRelatedState.recentlyVisitedThemes.mapD(thms => thms.filter(th => th !== state.layout.id).slice(0, 3)) | ||||||
|   let allowOtherThemes = state.featureSwitches.featureSwitchBackToThemeOverview |   let allowOtherThemes = state.featureSwitches.featureSwitchBackToThemeOverview | ||||||
|  |   let searchTerm = state.searchState.searchTerm | ||||||
|  |   let results = state.searchState.suggestions | ||||||
|  |   let filterResults = state.searchState.filterSuggestions | ||||||
|  |   let themeResults = state.searchState.themeSuggestions | ||||||
|  | 
 | ||||||
| </script> | </script> | ||||||
|  | <div class="p-4"> | ||||||
| 
 | 
 | ||||||
| <div class="relative w-full h-full collapsable " class:collapsed={!$isFocused && !$hasActiveFilters}> |   <ActiveFilters activeFilters={$activeFilters} /> | ||||||
|   <div class="searchbox normal-background"> |  | ||||||
|     <ActiveFilters activeFilters={$activeFilters} /> |  | ||||||
|     {#if $isFocused} |  | ||||||
|       {#if $searchTerm.length > 0 && results === undefined} |  | ||||||
|         <div class="flex justify-center m-4 my-8"> |  | ||||||
|           <Loading /> |  | ||||||
|         </div> |  | ||||||
|       {:else if results?.length > 0} |  | ||||||
|         <div style="max-height: calc(50vh - 1rem - 2px);" class="overflow-y-auto p-2" tabindex="-1"> |  | ||||||
| 
 | 
 | ||||||
|           {#each results as entry (entry)} |   {#if $filterResults.length > 0} | ||||||
|             <SearchResultSvelte on:select {entry} {state} /> |     <h3>Pick a filter below</h3> | ||||||
|  | 
 | ||||||
|  |     <div class="flex flex-wrap"> | ||||||
|  |       {#each $filterResults as filterResult (filterResult)} | ||||||
|  |         <FilterResult {state} entry={filterResult} /> | ||||||
|  |       {/each} | ||||||
|  |     </div> | ||||||
|  |   {/if} | ||||||
|  | 
 | ||||||
|  |   {#if $searchTerm.length > 0} | ||||||
|  |     <h3>Locations</h3> | ||||||
|  |   {/if} | ||||||
|  |   {#if $searchTerm.length > 0 && $results === undefined} | ||||||
|  |     <div class="flex justify-center m-4 my-8"> | ||||||
|  |       <Loading /> | ||||||
|  |     </div> | ||||||
|  |   {:else if $results?.length > 0} | ||||||
|  |     {#each $results as entry (entry)} | ||||||
|  |       <SearchResultSvelte on:select {entry} {state} /> | ||||||
|  |     {/each} | ||||||
|  |   {:else if $searchTerm.length > 0 || $recentlySeen?.length > 0 || $recentThemes?.length > 0} | ||||||
|  |     <div class="flex flex-col gap-y-8" | ||||||
|  |          tabindex="-1"> | ||||||
|  |       {#if $searchTerm.length > 0} | ||||||
|  |         <b class="flex justify-center p-4"> | ||||||
|  |           <Tr t={Translations.t.general.search.nothingFor.Subs({term: $searchTerm})} /> | ||||||
|  |         </b> | ||||||
|  |       {/if} | ||||||
|  | 
 | ||||||
|  |       {#if $recentlySeen?.length > 0} | ||||||
|  |         <div> | ||||||
|  |           <h3 class="m-2"> | ||||||
|  |             <Tr t={Translations.t.general.search.recents} /> | ||||||
|  |           </h3> | ||||||
|  |           {#each $recentlySeen as entry} | ||||||
|  |             <SearchResultSvelte {entry} {state} on:select /> | ||||||
|           {/each} |           {/each} | ||||||
|         </div> |         </div> | ||||||
|       {:else if $searchTerm.length > 0 || $recentlySeen?.length > 0 || $recentThemes?.length > 0} |       {/if} | ||||||
|         <div style="max-height: calc(50vh - 1rem - 2px);" class="overflow-y-auto p-2 flex flex-col gap-y-8" |  | ||||||
|              tabindex="-1"> |  | ||||||
|           {#if $searchTerm.length > 0} |  | ||||||
|             <b class="flex justify-center p-4"> |  | ||||||
|               <Tr t={Translations.t.general.search.nothingFor.Subs({term: $searchTerm})} /> |  | ||||||
|             </b> |  | ||||||
|           {/if} |  | ||||||
| 
 | 
 | ||||||
|           {#if $recentlySeen?.length > 0} |       {#if $recentThemes?.length > 0 && $allowOtherThemes} | ||||||
|             <div> |         <div> | ||||||
|               <h3 class="m-2"> |           <h3 class="m-2"> | ||||||
|                 <Tr t={Translations.t.general.search.recents} /> |             <Tr t={Translations.t.general.search.recentThemes} /> | ||||||
|               </h3> |           </h3> | ||||||
|               {#each $recentlySeen as entry} |           {#each $recentThemes as themeId (themeId)} | ||||||
|                 <SearchResultSvelte {entry} {state} on:select /> |             <SearchResultSvelte | ||||||
|               {/each} |               entry={{payload: MoreScreen.officialThemesById.get(themeId), osm_id: themeId, category: "theme"}} | ||||||
|             </div> |               {state} | ||||||
|           {/if} |               on:select /> | ||||||
| 
 |           {/each} | ||||||
|           {#if $recentThemes?.length > 0 && $allowOtherThemes} |  | ||||||
|             <div> |  | ||||||
|               <h3 class="m-2"> |  | ||||||
|                 <Tr t={Translations.t.general.search.recentThemes} /> |  | ||||||
|               </h3> |  | ||||||
|               {#each $recentThemes as themeId (themeId)} |  | ||||||
|                 <SearchResultSvelte |  | ||||||
|                   entry={{payload: MoreScreen.officialThemesById.get(themeId), osm_id: themeId, category: "theme"}} |  | ||||||
|                   {state} |  | ||||||
|                   on:select /> |  | ||||||
|               {/each} |  | ||||||
|             </div> |  | ||||||
|           {/if} |  | ||||||
|         </div> |         </div> | ||||||
|       {/if} |       {/if} | ||||||
|     {/if} |     </div> | ||||||
|   </div> |   {/if} | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |   {#if $themeResults.length > 0} | ||||||
|  |     <h3> | ||||||
|  |       Other maps | ||||||
|  |     </h3> | ||||||
|  |     {#each $themeResults as entry} | ||||||
|  |       <ThemeResult {state} {entry} /> | ||||||
|  |     {/each} | ||||||
|  |   {/if} | ||||||
|  | 
 | ||||||
| </div> | </div> | ||||||
| 
 |  | ||||||
| <style> |  | ||||||
|     .searchbox { |  | ||||||
|         display: flex; |  | ||||||
|         flex-direction: column; |  | ||||||
|         row-gap: 0.5rem; |  | ||||||
|         padding: 0.5rem; |  | ||||||
|         border: 1px solid black; |  | ||||||
|         border-radius: 0.5rem; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     .collapsable { |  | ||||||
|         max-height: 50vh; |  | ||||||
|         transition: max-height 400ms linear; |  | ||||||
|         transition-delay: 100ms; |  | ||||||
|         overflow: hidden; |  | ||||||
|         padding: 0 !important; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     .collapsed { |  | ||||||
|         padding-top: 0 !important; |  | ||||||
|         padding-bottom: 0 !important; |  | ||||||
|         max-height: 0 !important; |  | ||||||
|     } |  | ||||||
| </style> |  | ||||||
|  |  | ||||||
|  | @ -5,8 +5,8 @@ | ||||||
|   import Icon from "../Map/Icon.svelte" |   import Icon from "../Map/Icon.svelte" | ||||||
|   import Tr from "../Base/Tr.svelte" |   import Tr from "../Base/Tr.svelte" | ||||||
| 
 | 
 | ||||||
|   export let entry:  { category: "theme", payload: MinimalLayoutInformation } |   export let entry:   MinimalLayoutInformation | ||||||
|   let otherTheme = entry.payload |   let otherTheme = entry | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <a href={MoreScreen.createUrlFor(otherTheme, false)} | <a href={MoreScreen.createUrlFor(otherTheme, false)} | ||||||
|  |  | ||||||
|  | @ -29,7 +29,7 @@ import LayoutSource from "../Logic/FeatureSource/Sources/LayoutSource" | ||||||
| import { Map as MlMap } from "maplibre-gl" | import { Map as MlMap } from "maplibre-gl" | ||||||
| import ShowDataLayer from "./Map/ShowDataLayer" | import ShowDataLayer from "./Map/ShowDataLayer" | ||||||
| import { CombinedFetcher } from "../Logic/Web/NearbyImagesSearch" | import { CombinedFetcher } from "../Logic/Web/NearbyImagesSearch" | ||||||
| import { RecentSearch } from "../Logic/Geocoding/RecentSearch" | import SearchState from "../Logic/State/SearchState" | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * The state needed to render a special Visualisation. |  * The state needed to render a special Visualisation. | ||||||
|  | @ -95,7 +95,7 @@ export interface SpecialVisualizationState { | ||||||
|     readonly previewedImage: UIEventSource<ProvidedImage> |     readonly previewedImage: UIEventSource<ProvidedImage> | ||||||
|     readonly nearbyImageSearcher: CombinedFetcher |     readonly nearbyImageSearcher: CombinedFetcher | ||||||
|     readonly geolocation: GeoLocationHandler |     readonly geolocation: GeoLocationHandler | ||||||
|     readonly recentlySearched: RecentSearch |     readonly searchState: SearchState | ||||||
| 
 | 
 | ||||||
|     getMatchingLayer(properties: Record<string, string>); |     getMatchingLayer(properties: Record<string, string>); | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -13,9 +13,7 @@ | ||||||
|   import type { MapProperties } from "../Models/MapProperties" |   import type { MapProperties } from "../Models/MapProperties" | ||||||
|   import Geosearch from "./Search/Geosearch.svelte" |   import Geosearch from "./Search/Geosearch.svelte" | ||||||
|   import Translations from "./i18n/Translations" |   import Translations from "./i18n/Translations" | ||||||
|   import { |   import { MenuIcon } from "@rgossiaux/svelte-heroicons/solid" | ||||||
|     MenuIcon |  | ||||||
|   } from "@rgossiaux/svelte-heroicons/solid" |  | ||||||
|   import Tr from "./Base/Tr.svelte" |   import Tr from "./Base/Tr.svelte" | ||||||
|   import FloatOver from "./Base/FloatOver.svelte" |   import FloatOver from "./Base/FloatOver.svelte" | ||||||
|   import Constants from "../Models/Constants" |   import Constants from "../Models/Constants" | ||||||
|  | @ -41,13 +39,13 @@ | ||||||
|   import ReverseGeocoding from "./BigComponents/ReverseGeocoding.svelte" |   import ReverseGeocoding from "./BigComponents/ReverseGeocoding.svelte" | ||||||
|   import { BBox } from "../Logic/BBox" |   import { BBox } from "../Logic/BBox" | ||||||
|   import ExtraLinkButton from "./BigComponents/ExtraLinkButton.svelte" |   import ExtraLinkButton from "./BigComponents/ExtraLinkButton.svelte" | ||||||
|   import { LastClickFeatureSource } from "../Logic/FeatureSource/Sources/LastClickFeatureSource" |  | ||||||
|   import Marker from "./Map/Marker.svelte" |   import Marker from "./Map/Marker.svelte" | ||||||
|   import SelectedElementPanel from "./Base/SelectedElementPanel.svelte" |   import SelectedElementPanel from "./Base/SelectedElementPanel.svelte" | ||||||
|   import MenuDrawer from "./BigComponents/MenuDrawer.svelte" |   import MenuDrawer from "./BigComponents/MenuDrawer.svelte" | ||||||
|   import DrawerLeft from "./Base/DrawerLeft.svelte" |   import DrawerLeft from "./Base/DrawerLeft.svelte" | ||||||
|   import type { LayerConfigJson } from "../Models/ThemeConfig/Json/LayerConfigJson" |   import DrawerRight from "./Base/DrawerRight.svelte" | ||||||
|   import { GeocodingUtils } from "../Logic/Geocoding/GeocodingProvider" |   import SearchResults from "./Search/SearchResults.svelte" | ||||||
|  |   import { CloseButton } from "flowbite-svelte" | ||||||
| 
 | 
 | ||||||
|   export let state: ThemeViewState |   export let state: ThemeViewState | ||||||
|   let layout = state.layout |   let layout = state.layout | ||||||
|  | @ -174,107 +172,6 @@ | ||||||
|       /> |       /> | ||||||
|     </div> |     </div> | ||||||
|   {/if} |   {/if} | ||||||
| 
 |  | ||||||
|   <div class="pointer-events-none absolute top-0 left-0 w-full"> |  | ||||||
|     <!-- Top components --> |  | ||||||
| 
 |  | ||||||
|     <div |  | ||||||
|       class="flex bg-black-light-transparent pointer-events-auto items-center justify-between px-4 py-1 flex-wrap-reverse"> |  | ||||||
|       <!-- Top bar with tools --> |  | ||||||
|       <div class="flex items-center"> |  | ||||||
| 
 |  | ||||||
|         <MapControlButton |  | ||||||
|           cls="m-0.5 p-0.5 sm:p-1" |  | ||||||
|           arialabel={Translations.t.general.labels.menu} |  | ||||||
|           on:click={() => {console.log("Opening...."); state.guistate.menuIsOpened.setData(true)}} |  | ||||||
|           on:keydown={forwardEventToMap} |  | ||||||
|         > |  | ||||||
|           <MenuIcon class="h-6 w-6 cursor-pointer" /> |  | ||||||
|         </MapControlButton> |  | ||||||
| 
 |  | ||||||
|         <MapControlButton |  | ||||||
|           on:click={() => state.guistate.pageStates.about_theme.set(true)} |  | ||||||
|           on:keydown={forwardEventToMap} |  | ||||||
|         > |  | ||||||
|           <div |  | ||||||
|             class="m-0.5 mx-1 flex cursor-pointer items-center max-[480px]:w-full sm:mx-1 mr-2" |  | ||||||
|           > |  | ||||||
|             <Marker icons={layout.icon} size="h-6 w-6 shrink-0 mr-0.5 sm:mr-1 md:mr-2" /> |  | ||||||
|             <b class="mr-1"> |  | ||||||
|               <Tr t={layout.title} /> |  | ||||||
|             </b> |  | ||||||
|           </div> |  | ||||||
|         </MapControlButton> |  | ||||||
|       </div> |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|       <If condition={state.featureSwitches.featureSwitchSearch}> |  | ||||||
|         <div class="w-full sm:w-64 my-2 sm:mt-0"> |  | ||||||
| 
 |  | ||||||
|           <Geosearch |  | ||||||
|             bounds={state.mapProperties.bounds} |  | ||||||
|             on:searchCompleted={() => { |  | ||||||
|             state.map?.data?.getCanvas()?.focus() |  | ||||||
|           }} |  | ||||||
|             perLayer={state.perLayer} |  | ||||||
|             selectedElement={state.selectedElement} |  | ||||||
|             geolocationState={state.geolocation.geolocationState} |  | ||||||
|             searcher={state.geosearch} |  | ||||||
|             {state} |  | ||||||
|           /> |  | ||||||
|         </div> |  | ||||||
|       </If> |  | ||||||
| 
 |  | ||||||
|     </div> |  | ||||||
| 
 |  | ||||||
|     <div class="pointer-events-auto float-right mt-1 flex flex-col px-1 max-[480px]:w-full sm:m-2"> |  | ||||||
|       <If condition={state.visualFeedback}> |  | ||||||
|         {#if $selectedElement === undefined} |  | ||||||
|           <div class="w-fit"> |  | ||||||
|             <VisualFeedbackPanel {state} /> |  | ||||||
|           </div> |  | ||||||
|         {/if} |  | ||||||
|       </If> |  | ||||||
| 
 |  | ||||||
|     </div> |  | ||||||
|     <div class="float-left m-1 flex flex-col sm:mt-2"> |  | ||||||
|       <If condition={state.featureSwitches.featureSwitchWelcomeMessage}> |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|       </If> |  | ||||||
|       {#if currentViewLayer?.tagRenderings && currentViewLayer.defaultIcon()} |  | ||||||
|         <MapControlButton |  | ||||||
|           on:click={() => { |  | ||||||
|             state.selectCurrentView() |  | ||||||
|           }} |  | ||||||
|           on:keydown={forwardEventToMap} |  | ||||||
|         > |  | ||||||
|           <div class="h-8 w-8 cursor-pointer"> |  | ||||||
|             <ToSvelte construct={() => currentViewLayer.defaultIcon()} /> |  | ||||||
|           </div> |  | ||||||
|         </MapControlButton> |  | ||||||
|       {/if} |  | ||||||
|       <ExtraLinkButton {state} /> |  | ||||||
|       <UploadingImageCounter featureId="*" showThankYou={false} {state} /> |  | ||||||
|       <PendingChangesIndicator {state} /> |  | ||||||
|       <If condition={state.featureSwitchIsTesting}> |  | ||||||
|         <div class="alert w-fit">Testmode</div> |  | ||||||
|       </If> |  | ||||||
|       {#if state.osmConnection.Backend().startsWith("https://master.apis.dev.openstreetmap.org")} |  | ||||||
|         <div class="thanks">Testserver</div> |  | ||||||
|       {/if} |  | ||||||
|       <If condition={state.featureSwitches.featureSwitchFakeUser}> |  | ||||||
|         <div class="alert w-fit">Faking a user (Testmode)</div> |  | ||||||
|       </If> |  | ||||||
|     </div> |  | ||||||
|     <div class="flex w-full flex-col items-center justify-center"> |  | ||||||
|       <!-- Flex and w-full are needed for the positioning --> |  | ||||||
|       <!-- Centermessage --> |  | ||||||
|       <StateIndicator {state} /> |  | ||||||
|       <ReverseGeocoding {state} /> |  | ||||||
|     </div> |  | ||||||
|   </div> |  | ||||||
| 
 |  | ||||||
|   <div class="pointer-events-none absolute bottom-0 left-0 mb-4 w-screen"> |   <div class="pointer-events-none absolute bottom-0 left-0 mb-4 w-screen"> | ||||||
|     <!-- bottom controls --> |     <!-- bottom controls --> | ||||||
|     <div class="flex w-full items-end justify-between px-4"> |     <div class="flex w-full items-end justify-between px-4"> | ||||||
|  | @ -380,8 +277,110 @@ | ||||||
|         </If> |         </If> | ||||||
|       </div> |       </div> | ||||||
|     </div> |     </div> | ||||||
|  | 
 | ||||||
|   </div> |   </div> | ||||||
| 
 | 
 | ||||||
|  | 
 | ||||||
|  |   <DrawerRight shown={state.searchState.showSearchDrawer} }> | ||||||
|  |     <div class="relative"> | ||||||
|  |       <div class="absolute right-0 top-0 "> | ||||||
|  |         <div class="mr-4 mt-4"> | ||||||
|  |           <CloseButton on:click={() => state.searchState.showSearchDrawer.set(false)} /> | ||||||
|  |         </div> | ||||||
|  |       </div> | ||||||
|  |       <SearchResults {state} /> | ||||||
|  |     </div> | ||||||
|  |   </DrawerRight> | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |   <div class="pointer-events-none absolute top-0 left-0 w-full"> | ||||||
|  |     <!-- Top components --> | ||||||
|  |     <div | ||||||
|  |       id="top-bar" | ||||||
|  |       class="flex bg-black-light-transparent pointer-events-auto items-center justify-between px-4 py-1 flex-wrap-reverse"> | ||||||
|  |       <!-- Top bar with tools --> | ||||||
|  |       <div class="flex items-center"> | ||||||
|  | 
 | ||||||
|  |         <MapControlButton | ||||||
|  |           cls="m-0.5 p-0.5 sm:p-1" | ||||||
|  |           arialabel={Translations.t.general.labels.menu} | ||||||
|  |           on:click={() => {state.guistate.menuIsOpened.setData(true)}} | ||||||
|  |           on:keydown={forwardEventToMap} | ||||||
|  |         > | ||||||
|  |           <MenuIcon class="h-6 w-6 cursor-pointer" /> | ||||||
|  |         </MapControlButton> | ||||||
|  | 
 | ||||||
|  |         <MapControlButton | ||||||
|  |           on:click={() => state.guistate.pageStates.about_theme.set(true)} | ||||||
|  |           on:keydown={forwardEventToMap} | ||||||
|  |         > | ||||||
|  |           <div | ||||||
|  |             class="m-0.5 mx-1 flex cursor-pointer items-center max-[480px]:w-full sm:mx-1 mr-2" | ||||||
|  |           > | ||||||
|  |             <Marker icons={layout.icon} size="h-6 w-6 shrink-0 mr-0.5 sm:mr-1 md:mr-2" /> | ||||||
|  |             <b class="mr-1"> | ||||||
|  |               <Tr t={layout.title} /> | ||||||
|  |             </b> | ||||||
|  |           </div> | ||||||
|  |         </MapControlButton> | ||||||
|  |       </div> | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |       <If condition={state.featureSwitches.featureSwitchSearch}> | ||||||
|  |         <div class="w-full sm:w-80 md:w-96 my-2 sm:mt-0"> | ||||||
|  |           <Geosearch {state} isFocused={state.searchState.searchIsFocused} | ||||||
|  |                      searchContents={state.searchState.searchTerm} /> | ||||||
|  |         </div> | ||||||
|  |       </If> | ||||||
|  | 
 | ||||||
|  |     </div> | ||||||
|  | 
 | ||||||
|  |     <div class="pointer-events-auto float-right mt-1 flex flex-col px-1 max-[480px]:w-full sm:m-2"> | ||||||
|  |       <If condition={state.visualFeedback}> | ||||||
|  |         {#if $selectedElement === undefined} | ||||||
|  |           <div class="w-fit"> | ||||||
|  |             <VisualFeedbackPanel {state} /> | ||||||
|  |           </div> | ||||||
|  |         {/if} | ||||||
|  |       </If> | ||||||
|  | 
 | ||||||
|  |     </div> | ||||||
|  |     <div class="float-left m-1 flex flex-col sm:mt-2"> | ||||||
|  | 
 | ||||||
|  |       {#if currentViewLayer?.tagRenderings && currentViewLayer.defaultIcon()} | ||||||
|  |         <MapControlButton | ||||||
|  |           on:click={() => { | ||||||
|  |             state.selectCurrentView() | ||||||
|  |           }} | ||||||
|  |           on:keydown={forwardEventToMap} | ||||||
|  |         > | ||||||
|  |           <div class="h-8 w-8 cursor-pointer"> | ||||||
|  |             <ToSvelte construct={() => currentViewLayer.defaultIcon()} /> | ||||||
|  |           </div> | ||||||
|  |         </MapControlButton> | ||||||
|  |       {/if} | ||||||
|  |       <ExtraLinkButton {state} /> | ||||||
|  |       <UploadingImageCounter featureId="*" showThankYou={false} {state} /> | ||||||
|  |       <PendingChangesIndicator {state} /> | ||||||
|  |       <If condition={state.featureSwitchIsTesting}> | ||||||
|  |         <div class="alert w-fit">Testmode</div> | ||||||
|  |       </If> | ||||||
|  |       {#if state.osmConnection.Backend().startsWith("https://master.apis.dev.openstreetmap.org")} | ||||||
|  |         <div class="thanks">Testserver</div> | ||||||
|  |       {/if} | ||||||
|  |       <If condition={state.featureSwitches.featureSwitchFakeUser}> | ||||||
|  |         <div class="alert w-fit">Faking a user (Testmode)</div> | ||||||
|  |       </If> | ||||||
|  |     </div> | ||||||
|  |     <div class="flex w-full flex-col items-center justify-center"> | ||||||
|  |       <!-- Flex and w-full are needed for the positioning --> | ||||||
|  |       <!-- Centermessage --> | ||||||
|  |       <StateIndicator {state} /> | ||||||
|  |       <ReverseGeocoding {state} /> | ||||||
|  |     </div> | ||||||
|  |   </div> | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|   <LoginToggle ignoreLoading={true} {state}> |   <LoginToggle ignoreLoading={true} {state}> | ||||||
|     {#if ($showCrosshair === "yes" && $currentZoom >= 17) || $showCrosshair === "always" || $visualFeedback} |     {#if ($showCrosshair === "yes" && $currentZoom >= 17) || $showCrosshair === "always" || $visualFeedback} | ||||||
|       <!-- Don't use h-full: h-full does _not_ include the area under the URL-bar, which offsets the crosshair a bit --> |       <!-- Don't use h-full: h-full does _not_ include the area under the URL-bar, which offsets the crosshair a bit --> | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue