forked from MapComplete/MapComplete
		
	Search: use 'searchbar' where applicable, refactoring
This commit is contained in:
		
							parent
							
								
									bcd53405c8
								
							
						
					
					
						commit
						9b8c300e77
					
				
					 28 changed files with 403 additions and 582 deletions
				
			
		|  | @ -1,12 +1,12 @@ | |||
| import GeocodingProvider, { SearchResult, GeocodingOptions } from "./GeocodingProvider" | ||||
| import GeocodingProvider, { SearchResult, GeocodingOptions, GeocodeResult } from "./GeocodingProvider" | ||||
| import { Utils } from "../../Utils" | ||||
| import { Store, Stores } from "../UIEventSource" | ||||
| 
 | ||||
| export default class CombinedSearcher implements GeocodingProvider { | ||||
|     private _providers: ReadonlyArray<GeocodingProvider> | ||||
|     private _providersWithSuggest: ReadonlyArray<GeocodingProvider> | ||||
| export default class CombinedSearcher implements GeocodingProvider <GeocodeResult> { | ||||
|     private _providers: ReadonlyArray<GeocodingProvider<GeocodeResult>> | ||||
|     private _providersWithSuggest: ReadonlyArray<GeocodingProvider<GeocodeResult>> | ||||
| 
 | ||||
|     constructor(...providers: ReadonlyArray<GeocodingProvider>) { | ||||
|     constructor(...providers: ReadonlyArray<GeocodingProvider<GeocodeResult>>) { | ||||
|         this._providers = Utils.NoNull(providers) | ||||
|         this._providersWithSuggest = this._providers.filter(pr => pr.suggest !== undefined) | ||||
|     } | ||||
|  | @ -17,10 +17,13 @@ export default class CombinedSearcher implements GeocodingProvider { | |||
|      * @param geocoded | ||||
|      * @private | ||||
|      */ | ||||
|     private merge(geocoded: SearchResult[][]): SearchResult[] { | ||||
|         const results: SearchResult[] = [] | ||||
|     public static merge(geocoded: GeocodeResult[][]): GeocodeResult[] { | ||||
|         const results: GeocodeResult[] = [] | ||||
|         const seenIds = new Set<string>() | ||||
|         for (const geocodedElement of geocoded) { | ||||
|             if(geocodedElement === undefined){ | ||||
|                 continue | ||||
|             } | ||||
|             for (const entry of geocodedElement) { | ||||
| 
 | ||||
| 
 | ||||
|  | @ -40,13 +43,13 @@ export default class CombinedSearcher implements GeocodingProvider { | |||
| 
 | ||||
|     async search(query: string, options?: GeocodingOptions): Promise<SearchResult[]> { | ||||
|         const results = (await Promise.all(this._providers.map(pr => pr.search(query, options)))) | ||||
|         return this.merge(results) | ||||
|         return CombinedSearcher.merge(results) | ||||
|     } | ||||
| 
 | ||||
|     suggest(query: string, options?: GeocodingOptions): Store<SearchResult[]> { | ||||
|         return Stores.concat( | ||||
|             this._providersWithSuggest.map(pr => pr.suggest(query, options))) | ||||
|             .map(gcrss => this.merge(gcrss)) | ||||
|             .map(gcrss => CombinedSearcher.merge(gcrss)) | ||||
| 
 | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -1,11 +1,11 @@ | |||
| import GeocodingProvider, { SearchResult } from "./GeocodingProvider" | ||||
| import GeocodingProvider, { GeocodeResult } from "./GeocodingProvider" | ||||
| import { Utils } from "../../Utils" | ||||
| import { ImmutableStore, Store } from "../UIEventSource" | ||||
| 
 | ||||
| /** | ||||
|  * A simple search-class which interprets possible locations | ||||
|  */ | ||||
| export default class CoordinateSearch implements GeocodingProvider { | ||||
| export default class CoordinateSearch implements GeocodingProvider<GeocodeResult> { | ||||
|     private static readonly latLonRegexes: ReadonlyArray<RegExp> = [ | ||||
|         /^(-?[0-9]+\.[0-9]+)[ ,;/\\]+(-?[0-9]+\.[0-9]+)/, | ||||
|         /lat[:=]? *['"]?(-?[0-9]+\.[0-9]+)['"]?[ ,;&]+lon[:=]? *['"]?(-?[0-9]+\.[0-9]+)['"]?/, | ||||
|  | @ -59,9 +59,9 @@ export default class CoordinateSearch implements GeocodingProvider { | |||
|      * results.length // => 1
 | ||||
|      * results[0] // => {lat: -57.5802905, lon: -12.7202538, display_name: "lon: -12.7202538, lat: -57.5802905",  "category": "coordinate", "source": "coordinate:latlon"}
 | ||||
|      */ | ||||
|     private directSearch(query: string): SearchResult[] { | ||||
|     private directSearch(query: string): GeocodeResult[] { | ||||
| 
 | ||||
|         const matches = Utils.NoNull(CoordinateSearch.latLonRegexes.map(r => query.match(r))).map(m => <SearchResult>{ | ||||
|         const matches = Utils.NoNull(CoordinateSearch.latLonRegexes.map(r => query.match(r))).map(m => <GeocodeResult>{ | ||||
|             lat: Number(m[1]), | ||||
|             lon: Number(m[2]), | ||||
|             display_name: "lon: " + m[2] + ", lat: " + m[1], | ||||
|  | @ -71,7 +71,7 @@ export default class CoordinateSearch implements GeocodingProvider { | |||
| 
 | ||||
| 
 | ||||
|         const matchesLonLat = Utils.NoNull(CoordinateSearch.lonLatRegexes.map(r => query.match(r))) | ||||
|             .map(m => <SearchResult>{ | ||||
|             .map(m => <GeocodeResult>{ | ||||
|                 lat: Number(m[2]), | ||||
|                 lon: Number(m[1]), | ||||
|                 display_name: "lon: " + m[1] + ", lat: " + m[2], | ||||
|  | @ -81,11 +81,11 @@ export default class CoordinateSearch implements GeocodingProvider { | |||
|         return matches.concat(matchesLonLat) | ||||
|     } | ||||
| 
 | ||||
|     suggest(query: string): Store<SearchResult[]> { | ||||
|     suggest(query: string): Store<GeocodeResult[]> { | ||||
|         return new ImmutableStore(this.directSearch(query)) | ||||
|     } | ||||
| 
 | ||||
|     async search (query: string): Promise<SearchResult[]> { | ||||
|     async search (query: string): Promise<GeocodeResult[]> { | ||||
|         return this.directSearch(query) | ||||
|     } | ||||
| 
 | ||||
|  |  | |||
|  | @ -3,13 +3,15 @@ import GeocodingProvider, { FilterPayload, FilterResult, GeocodingOptions, Searc | |||
| import { SpecialVisualizationState } from "../../UI/SpecialVisualization" | ||||
| import { Utils } from "../../Utils" | ||||
| import Locale from "../../UI/i18n/Locale" | ||||
| import Constants from "../../Models/Constants" | ||||
| 
 | ||||
| export default class FilterSearch implements GeocodingProvider { | ||||
|     private readonly _state: SpecialVisualizationState | ||||
|     private readonly suggestions | ||||
| 
 | ||||
|     constructor(state: SpecialVisualizationState) { | ||||
|         this._state = state | ||||
| 
 | ||||
|         this.suggestions = this.getSuggestions() | ||||
|     } | ||||
| 
 | ||||
|     async search(query: string): Promise<SearchResult[]> { | ||||
|  | @ -34,7 +36,6 @@ export default class FilterSearch implements GeocodingProvider { | |||
|             } | ||||
|             return query | ||||
|         }).filter(q => q.length > 0) | ||||
|         console.log("Queries:",queries) | ||||
|         const possibleFilters: FilterPayload[] = [] | ||||
|         for (const layer of this._state.layout.layers) { | ||||
|             if (!Array.isArray(layer.filters)) { | ||||
|  | @ -55,9 +56,9 @@ export default class FilterSearch implements GeocodingProvider { | |||
|                     terms = terms.map(t => Utils.simplifyStringForSearch(t)) | ||||
|                     terms.push(option.emoji) | ||||
|                     Utils.NoNullInplace(terms) | ||||
|                     const distances =    queries.flatMap(query => terms.map(entry => { | ||||
|                     const distances = queries.flatMap(query => terms.map(entry => { | ||||
|                         const d = Utils.levenshteinDistance(query, entry.slice(0, query.length)) | ||||
|                         console.log(query,"?  +",terms, "=",d) | ||||
|                         console.log(query, "?  +", terms, "=", d) | ||||
|                         const dRelative = d / query.length | ||||
|                         return dRelative | ||||
|                     })) | ||||
|  | @ -78,4 +79,37 @@ export default class FilterSearch implements GeocodingProvider { | |||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     getSuggestions(): FilterPayload[] { | ||||
|         if (this.suggestions) { | ||||
|        //     return this.suggestions
 | ||||
|         } | ||||
|         const result: FilterPayload[] = [] | ||||
|         for (const [id, filteredLayer] of this._state.layerState.filteredLayers) { | ||||
|             if (!Array.isArray(filteredLayer.layerDef.filters)) { | ||||
|                 continue | ||||
|             } | ||||
|             if (Constants.priviliged_layers.indexOf(id) >= 0) { | ||||
|                 continue | ||||
|             } | ||||
|             for (const filter of filteredLayer.layerDef.filters) { | ||||
|                 const singleFilterResults: FilterPayload[] = [] | ||||
|                 for (let i = 0; i < Math.min(filter.options.length, 5); i++) { | ||||
|                     const option = filter.options[i] | ||||
|                     if (option.osmTags === undefined) { | ||||
|                         continue | ||||
|                     } | ||||
|                     singleFilterResults.push({ | ||||
|                         option, | ||||
|                         filter, | ||||
|                         index: i, | ||||
|                         layer: filteredLayer.layerDef | ||||
|                     }) | ||||
|                 } | ||||
|                 Utils.shuffle(singleFilterResults) | ||||
|                 result.push(...singleFilterResults.slice(0,3)) | ||||
|             } | ||||
|         } | ||||
|         Utils.shuffle(result) | ||||
|         return result.slice(0,6) | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -56,16 +56,16 @@ export interface GeocodingOptions { | |||
| } | ||||
| 
 | ||||
| 
 | ||||
| export default interface GeocodingProvider { | ||||
| export default interface GeocodingProvider<T extends SearchResult = SearchResult> { | ||||
| 
 | ||||
| 
 | ||||
|     search(query: string, options?: GeocodingOptions): Promise<SearchResult[]> | ||||
|     search(query: string, options?: GeocodingOptions): Promise<T[]> | ||||
| 
 | ||||
|     /** | ||||
|      * @param query | ||||
|      * @param options | ||||
|      */ | ||||
|     suggest?(query: string, options?: GeocodingOptions): Store<SearchResult[]> | ||||
|     suggest?(query: string, options?: GeocodingOptions): Store<T[]> | ||||
| } | ||||
| 
 | ||||
| export type ReverseGeocodingResult = Feature<Geometry, { | ||||
|  |  | |||
|  | @ -1,10 +1,10 @@ | |||
| import { Store, UIEventSource } from "../UIEventSource" | ||||
| import GeocodingProvider, { SearchResult, GeocodingOptions } from "./GeocodingProvider" | ||||
| import GeocodingProvider, { GeocodingOptions, GeocodeResult } from "./GeocodingProvider" | ||||
| import { OsmId } from "../../Models/OsmFeature" | ||||
| import { SpecialVisualizationState } from "../../UI/SpecialVisualization" | ||||
| 
 | ||||
| export default class OpenStreetMapIdSearch implements GeocodingProvider { | ||||
|     private static readonly regex = /((https?:\/\/)?(www.)?(osm|openstreetmap).org\/)?(n|node|w|way|r|relation)[\/ ]?([0-9]+)/ | ||||
| export default class OpenStreetMapIdSearch implements GeocodingProvider<GeocodeResult> { | ||||
|     private static readonly regex = /((https?:\/\/)?(www.)?(osm|openstreetmap).org\/)?(n|node|w|way|r|relation)[/ ]?([0-9]+)/ | ||||
| 
 | ||||
|     private static readonly types: Readonly<Record<string, "node" | "way" | "relation">> = { | ||||
|         "n":"node", | ||||
|  | @ -45,7 +45,7 @@ export default class OpenStreetMapIdSearch implements GeocodingProvider { | |||
|         return undefined | ||||
|     } | ||||
| 
 | ||||
|     async search(query: string, options?: GeocodingOptions): Promise<SearchResult[]> { | ||||
|     async search(query: string, options?: GeocodingOptions): Promise<GeocodeResult[]> { | ||||
|         const id = OpenStreetMapIdSearch.extractId(query) | ||||
|         if (!id) { | ||||
|             return [] | ||||
|  | @ -74,7 +74,7 @@ export default class OpenStreetMapIdSearch implements GeocodingProvider { | |||
|         }] | ||||
|     } | ||||
| 
 | ||||
|     suggest?(query: string, options?: GeocodingOptions): Store<SearchResult[]> { | ||||
|     suggest?(query: string, options?: GeocodingOptions): Store<GeocodeResult[]> { | ||||
|         return UIEventSource.FromPromise(this.search(query, options)) | ||||
|     } | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,6 +1,11 @@ | |||
| import GeocodingProvider, { FilterPayload, GeocodingUtils, type SearchResult } from "../Geocoding/GeocodingProvider" | ||||
| import GeocodingProvider, { | ||||
|     FilterPayload, | ||||
|     GeocodeResult, | ||||
|     GeocodingUtils, | ||||
|     type SearchResult | ||||
| } from "../Geocoding/GeocodingProvider" | ||||
| import { RecentSearch } from "../Geocoding/RecentSearch" | ||||
| import { Store, Stores, UIEventSource } from "../UIEventSource" | ||||
| import { ImmutableStore, Store, Stores, UIEventSource } from "../UIEventSource" | ||||
| import CombinedSearcher from "../Geocoding/CombinedSearcher" | ||||
| import FilterSearch from "../Geocoding/FilterSearch" | ||||
| import LocalElementSearch from "../Geocoding/LocalElementSearch" | ||||
|  | @ -20,7 +25,6 @@ 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>("") | ||||
|  | @ -28,28 +32,40 @@ export default class SearchState { | |||
|     public readonly suggestions: Store<SearchResult[]> | ||||
|     public readonly filterSuggestions: Store<FilterPayload[]> | ||||
|     public readonly themeSuggestions: Store<MinimalLayoutInformation[]> | ||||
|     public readonly locationSearchers: ReadonlyArray<GeocodingProvider<GeocodeResult>> | ||||
| 
 | ||||
|     private readonly state: ThemeViewState | ||||
|     public readonly showSearchDrawer: UIEventSource<boolean> | ||||
|     public readonly suggestionsSearchRunning: Store<boolean> | ||||
| 
 | ||||
|     constructor(state: ThemeViewState) { | ||||
|         this.state = state | ||||
| 
 | ||||
|         this.geosearch = new CombinedSearcher( | ||||
|            // new LocalElementSearch(state, 5),
 | ||||
|         this.locationSearchers = [ | ||||
|             // 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 => { | ||||
|         const suggestionsList = this.searchTerm.stabilized(250).mapD(search => { | ||||
|                 if (search.length === 0) { | ||||
|                     return undefined | ||||
|                 } | ||||
|                 return Stores.holdDefined(bounds.bindD(bbox => this.geosearch.suggest(search, { bbox }))) | ||||
|                 return this.locationSearchers.map(ls => ls.suggest(search, { bbox: bounds.data })) | ||||
| 
 | ||||
|             }, [bounds] | ||||
|         ) | ||||
|         this.suggestionsSearchRunning =  suggestionsList.bind(suggestions => { | ||||
|             if(suggestions === undefined){ | ||||
|                 return new ImmutableStore(true) | ||||
|             } | ||||
|             return Stores.concat(suggestions).map(suggestions => suggestions.some(list => list === undefined)) | ||||
|         }) | ||||
|         this.suggestions = suggestionsList.bindD(suggestions => | ||||
|             Stores.concat(suggestions).map(suggestions => CombinedSearcher.merge(suggestions)) | ||||
|         ) | ||||
| 
 | ||||
|         const themeSearch = new ThemeSearch(state, 3) | ||||
|  | @ -57,8 +73,7 @@ export default class SearchState { | |||
| 
 | ||||
| 
 | ||||
|         const filterSearch = new FilterSearch(state) | ||||
|         this.filterSuggestions = this.searchTerm.stabilized(50).mapD(query => | ||||
|             filterSearch.searchDirectly(query) | ||||
|         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 }) => { | ||||
|  | @ -81,11 +96,7 @@ export default class SearchState { | |||
|         ) | ||||
| 
 | ||||
|         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) | ||||
|  |  | |||
|  | @ -41,17 +41,16 @@ export class Stores { | |||
|         return src | ||||
|     } | ||||
| 
 | ||||
|     public static concat<T>(stores: Store<T[]>[]): Store<T[][]> { | ||||
|         const newStore = new UIEventSource<T[][]>([]) | ||||
|         function update(){ | ||||
|             if(newStore._callbacks.isDestroyed){ | ||||
|     public static concat<T>(stores: Store<T[] | undefined>[]): Store<(T[] | undefined)[]> { | ||||
|         const newStore = new UIEventSource<(T[] | undefined)[]>([]) | ||||
| 
 | ||||
|         function update() { | ||||
|             if (newStore._callbacks.isDestroyed) { | ||||
|                 return true // unregister
 | ||||
|             } | ||||
|             const results: T[][] = [] | ||||
|             const results: (T[] | undefined)[] = [] | ||||
|             for (const store of stores) { | ||||
|                 if(store.data){ | ||||
|                     results.push(store.data) | ||||
|                 } | ||||
|                 results.push(store.data) | ||||
|             } | ||||
|             newStore.setData(results) | ||||
|         } | ||||
|  | @ -261,7 +260,7 @@ export abstract class Store<T> implements Readable<T> { | |||
|                 if (mapped.data === newEventSource) { | ||||
|                     sink.setData(resultData) | ||||
|                 } | ||||
|                 if(sink._callbacks.isDestroyed){ | ||||
|                 if (sink._callbacks.isDestroyed) { | ||||
|                     return true // unregister
 | ||||
|                 } | ||||
|             }) | ||||
|  | @ -270,7 +269,7 @@ export abstract class Store<T> implements Readable<T> { | |||
|         return sink | ||||
|     } | ||||
| 
 | ||||
|     public bindD<X>(f: (t: Exclude<T, undefined | null>) => Store<X>, extraSources: UIEventSource<object>[] =[]): Store<X> { | ||||
|     public bindD<X>(f: (t: Exclude<T, undefined | null>) => Store<X>, extraSources: UIEventSource<object>[] = []): Store<X> { | ||||
|         return this.bind((t) => { | ||||
|             if (t === null) { | ||||
|                 return null | ||||
|  | @ -408,7 +407,8 @@ export class ImmutableStore<T> extends Store<T> { | |||
| class ListenerTracker<T> { | ||||
|     public pingCount = 0 | ||||
|     private readonly _callbacks: ((t: T) => boolean | void | any)[] = [] | ||||
| public isDestroyed = false | ||||
|     public isDestroyed = false | ||||
| 
 | ||||
|     /** | ||||
|      * Adds a callback which can be called; a function to unregister is returned | ||||
|      */ | ||||
|  | @ -469,8 +469,8 @@ public isDestroyed = false | |||
|         return this._callbacks.length | ||||
|     } | ||||
| 
 | ||||
|     public destroy(){ | ||||
|         this.isDestroyed=  true | ||||
|     public destroy() { | ||||
|         this.isDestroyed = true | ||||
|         this._callbacks.splice(0, this._callbacks.length) | ||||
|     } | ||||
| } | ||||
|  | @ -635,7 +635,8 @@ class MappedStore<TIn, T> extends Store<T> { | |||
| } | ||||
| 
 | ||||
| export class UIEventSource<T> extends Store<T> implements Writable<T> { | ||||
|     private static readonly pass: (() => void) = () => {}; | ||||
|     private static readonly pass: (() => void) = () => { | ||||
|     } | ||||
|     public data: T | ||||
|     _callbacks: ListenerTracker<T> = new ListenerTracker<T>() | ||||
| 
 | ||||
|  | @ -644,7 +645,7 @@ export class UIEventSource<T> extends Store<T> implements Writable<T> { | |||
|         this.data = data | ||||
|     } | ||||
| 
 | ||||
|     public destroy(){ | ||||
|     public destroy() { | ||||
|         this._callbacks.destroy() | ||||
|     } | ||||
| 
 | ||||
|  | @ -782,9 +783,9 @@ export class UIEventSource<T> extends Store<T> implements Writable<T> { | |||
|                     return defaultV | ||||
|                 } | ||||
|                 try { | ||||
|                     return <T> JSON.parse(str) | ||||
|                     return <T>JSON.parse(str) | ||||
|                 } catch (e) { | ||||
|                     console.error("Could not parse value", str,"due to",e) | ||||
|                     console.error("Could not parse value", str, "due to", e) | ||||
|                     return defaultV | ||||
|                 } | ||||
|             }, | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue