forked from MapComplete/MapComplete
		
	Feature(geocoding): pressing enter will now zoom to the first search result; refactor away type synonym
This commit is contained in:
		
							parent
							
								
									9e8aaab086
								
							
						
					
					
						commit
						686ad70511
					
				
					 8 changed files with 64 additions and 49 deletions
				
			
		|  | @ -1,8 +1,4 @@ | ||||||
| import GeocodingProvider, { | import GeocodingProvider, { GeocodeResult, GeocodingOptions } from "./GeocodingProvider" | ||||||
|     GeocodeResult, |  | ||||||
|     GeocodingOptions, |  | ||||||
|     SearchResult, |  | ||||||
| } from "./GeocodingProvider" |  | ||||||
| import { Utils } from "../../Utils" | import { Utils } from "../../Utils" | ||||||
| import { Store, Stores } from "../UIEventSource" | import { Store, Stores } from "../UIEventSource" | ||||||
| 
 | 
 | ||||||
|  | @ -44,12 +40,12 @@ export default class CombinedSearcher implements GeocodingProvider { | ||||||
|         return results |         return results | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     async search(query: string, options?: GeocodingOptions): Promise<SearchResult[]> { |     async search(query: string, options?: GeocodingOptions): Promise<GeocodeResult[]> { | ||||||
|         const results = await Promise.all(this._providers.map((pr) => pr.search(query, options))) |         const results = await Promise.all(this._providers.map((pr) => pr.search(query, options))) | ||||||
|         return CombinedSearcher.merge(results) |         return CombinedSearcher.merge(results) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     suggest(query: string, options?: GeocodingOptions): Store<SearchResult[]> { |     suggest(query: string, options?: GeocodingOptions): Store<GeocodeResult[]> { | ||||||
|         return Stores.concat( |         return Stores.concat( | ||||||
|             this._providersWithSuggest.map((pr) => pr.suggest(query, options)) |             this._providersWithSuggest.map((pr) => pr.suggest(query, options)) | ||||||
|         ).map((gcrss) => CombinedSearcher.merge(gcrss)) |         ).map((gcrss) => CombinedSearcher.merge(gcrss)) | ||||||
|  |  | ||||||
|  | @ -1,4 +1,4 @@ | ||||||
| import { SearchResult } from "./GeocodingProvider" | import { GeocodeResult } from "./GeocodingProvider" | ||||||
| import { Store } from "../UIEventSource" | import { Store } from "../UIEventSource" | ||||||
| import { FeatureSource } from "../FeatureSource/FeatureSource" | import { FeatureSource } from "../FeatureSource/FeatureSource" | ||||||
| import { Feature, Geometry } from "geojson" | import { Feature, Geometry } from "geojson" | ||||||
|  | @ -6,7 +6,7 @@ import { Feature, Geometry } from "geojson" | ||||||
| export default class GeocodingFeatureSource implements FeatureSource { | export default class GeocodingFeatureSource implements FeatureSource { | ||||||
|     public features: Store<Feature<Geometry, Record<string, string>>[]> |     public features: Store<Feature<Geometry, Record<string, string>>[]> | ||||||
| 
 | 
 | ||||||
|     constructor(provider: Store<SearchResult[]>) { |     constructor(provider: Store<GeocodeResult[]>) { | ||||||
|         this.features = provider.map((geocoded) => { |         this.features = provider.map((geocoded) => { | ||||||
|             if (geocoded === undefined) { |             if (geocoded === undefined) { | ||||||
|                 return [] |                 return [] | ||||||
|  |  | ||||||
|  | @ -42,7 +42,6 @@ export type GeocodeResult = { | ||||||
|     payload?: object |     payload?: object | ||||||
|     source?: string |     source?: string | ||||||
| } | } | ||||||
| export type SearchResult = GeocodeResult |  | ||||||
| 
 | 
 | ||||||
| export interface GeocodingOptions { | export interface GeocodingOptions { | ||||||
|     bbox?: BBox |     bbox?: BBox | ||||||
|  |  | ||||||
|  | @ -1,4 +1,4 @@ | ||||||
| import GeocodingProvider, { GeocodingOptions, SearchResult } from "./GeocodingProvider" | import GeocodingProvider, { GeocodeResult, GeocodingOptions } from "./GeocodingProvider" | ||||||
| import ThemeViewState from "../../Models/ThemeViewState" | import ThemeViewState from "../../Models/ThemeViewState" | ||||||
| import { Utils } from "../../Utils" | import { Utils } from "../../Utils" | ||||||
| import { Feature } from "geojson" | import { Feature } from "geojson" | ||||||
|  | @ -26,7 +26,7 @@ export default class LocalElementSearch implements GeocodingProvider { | ||||||
|         this._limit = limit |         this._limit = limit | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     async search(query: string, options?: GeocodingOptions): Promise<SearchResult[]> { |     async search(query: string, options?: GeocodingOptions): Promise<GeocodeResult[]> { | ||||||
|         return this.searchEntries(query, options, false).data |         return this.searchEntries(query, options, false).data | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | @ -92,7 +92,7 @@ export default class LocalElementSearch implements GeocodingProvider { | ||||||
|         query: string, |         query: string, | ||||||
|         options?: GeocodingOptions, |         options?: GeocodingOptions, | ||||||
|         matchStart?: boolean |         matchStart?: boolean | ||||||
|     ): Store<SearchResult[]> { |     ): Store<GeocodeResult[]> { | ||||||
|         if (query.length < 3) { |         if (query.length < 3) { | ||||||
|             return new ImmutableStore([]) |             return new ImmutableStore([]) | ||||||
|         } |         } | ||||||
|  | @ -126,7 +126,7 @@ export default class LocalElementSearch implements GeocodingProvider { | ||||||
|             } |             } | ||||||
|             return results.map((entry) => { |             return results.map((entry) => { | ||||||
|                 const [osm_type, osm_id] = entry.feature.properties.id.split("/") |                 const [osm_type, osm_id] = entry.feature.properties.id.split("/") | ||||||
|                 return <SearchResult>{ |                 return <GeocodeResult>{ | ||||||
|                     lon: entry.center[0], |                     lon: entry.center[0], | ||||||
|                     lat: entry.center[1], |                     lat: entry.center[1], | ||||||
|                     osm_type, |                     osm_type, | ||||||
|  | @ -141,7 +141,7 @@ export default class LocalElementSearch implements GeocodingProvider { | ||||||
|         }) |         }) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     suggest(query: string, options?: GeocodingOptions): Store<SearchResult[]> { |     suggest(query: string, options?: GeocodingOptions): Store<GeocodeResult[]> { | ||||||
|         return this.searchEntries(query, options, true) |         return this.searchEntries(query, options, true) | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -3,7 +3,7 @@ import { BBox } from "../BBox" | ||||||
| import Constants from "../../Models/Constants" | import Constants from "../../Models/Constants" | ||||||
| import { FeatureCollection } from "geojson" | import { FeatureCollection } from "geojson" | ||||||
| import Locale from "../../UI/i18n/Locale" | import Locale from "../../UI/i18n/Locale" | ||||||
| import GeocodingProvider, { GeocodingOptions, SearchResult } from "./GeocodingProvider" | import GeocodingProvider, { GeocodeResult, GeocodingOptions } from "./GeocodingProvider" | ||||||
| 
 | 
 | ||||||
| export class NominatimGeocoding implements GeocodingProvider { | export class NominatimGeocoding implements GeocodingProvider { | ||||||
|     private readonly _host |     private readonly _host | ||||||
|  | @ -15,7 +15,7 @@ export class NominatimGeocoding implements GeocodingProvider { | ||||||
|         this._host = host |         this._host = host | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public search(query: string, options?: GeocodingOptions): Promise<SearchResult[]> { |     public search(query: string, options?: GeocodingOptions): Promise<GeocodeResult[]> { | ||||||
|         const b = options?.bbox ?? BBox.global |         const b = options?.bbox ?? BBox.global | ||||||
|         const url = `${this._host}search?format=json&limit=${ |         const url = `${this._host}search?format=json&limit=${ | ||||||
|             this.limit |             this.limit | ||||||
|  |  | ||||||
|  | @ -1,4 +1,4 @@ | ||||||
| import GeocodingProvider, { type SearchResult } from "../Search/GeocodingProvider" | import GeocodingProvider, { GeocodeResult, GeocodingUtils } from "../Search/GeocodingProvider" | ||||||
| import { ImmutableStore, Store, Stores, UIEventSource } from "../UIEventSource" | import { ImmutableStore, Store, Stores, UIEventSource } from "../UIEventSource" | ||||||
| import CombinedSearcher from "../Search/CombinedSearcher" | import CombinedSearcher from "../Search/CombinedSearcher" | ||||||
| import FilterSearch, { FilterSearchResult } from "../Search/FilterSearch" | import FilterSearch, { FilterSearchResult } from "../Search/FilterSearch" | ||||||
|  | @ -16,12 +16,13 @@ import LayerConfig from "../../Models/ThemeConfig/LayerConfig" | ||||||
| import { FeatureSource } from "../FeatureSource/FeatureSource" | import { FeatureSource } from "../FeatureSource/FeatureSource" | ||||||
| import { Feature } from "geojson" | import { Feature } from "geojson" | ||||||
| import OpenLocationCodeSearch from "../Search/OpenLocationCodeSearch" | import OpenLocationCodeSearch from "../Search/OpenLocationCodeSearch" | ||||||
|  | import { BBox } from "../BBox" | ||||||
| 
 | 
 | ||||||
| export default class SearchState { | export default class SearchState { | ||||||
|     public readonly feedback: UIEventSource<Translation> = new UIEventSource<Translation>(undefined) |     public readonly feedback: UIEventSource<Translation> = new UIEventSource<Translation>(undefined) | ||||||
|     public readonly searchTerm: UIEventSource<string> = new UIEventSource<string>("") |     public readonly searchTerm: UIEventSource<string> = new UIEventSource<string>("") | ||||||
|     public readonly searchIsFocused = new UIEventSource(false) |     public readonly searchIsFocused = new UIEventSource(false) | ||||||
|     public readonly suggestions: Store<SearchResult[]> |     public readonly suggestions: Store<GeocodeResult[]> | ||||||
|     public readonly filterSuggestions: Store<FilterSearchResult[]> |     public readonly filterSuggestions: Store<FilterSearchResult[]> | ||||||
|     public readonly themeSuggestions: Store<MinimalThemeInformation[]> |     public readonly themeSuggestions: Store<MinimalThemeInformation[]> | ||||||
|     public readonly layerSuggestions: Store<LayerConfig[]> |     public readonly layerSuggestions: Store<LayerConfig[]> | ||||||
|  | @ -60,7 +61,7 @@ export default class SearchState { | ||||||
|                 return new ImmutableStore(true) |                 return new ImmutableStore(true) | ||||||
|             } |             } | ||||||
|             return Stores.concat(suggestions).map((suggestions) => |             return Stores.concat(suggestions).map((suggestions) => | ||||||
|                 suggestions.some((list, i) => list === undefined) |                 suggestions.some(list => list === undefined) | ||||||
|             ) |             ) | ||||||
|         }) |         }) | ||||||
|         this.suggestions = suggestionsList.bindD((suggestions) => |         this.suggestions = suggestionsList.bindD((suggestions) => | ||||||
|  | @ -100,7 +101,7 @@ export default class SearchState { | ||||||
| 
 | 
 | ||||||
|         this.showSearchDrawer = new UIEventSource(false) |         this.showSearchDrawer = new UIEventSource(false) | ||||||
| 
 | 
 | ||||||
|         this.searchIsFocused.addCallbackAndRunD((sugg) => { |         this.searchIsFocused.addCallbackAndRunD(sugg => { | ||||||
|             if (sugg) { |             if (sugg) { | ||||||
|                 this.showSearchDrawer.set(true) |                 this.showSearchDrawer.set(true) | ||||||
|             } |             } | ||||||
|  | @ -124,7 +125,6 @@ export default class SearchState { | ||||||
|         const state = this.state |         const state = this.state | ||||||
| 
 | 
 | ||||||
|         const layersToShow = payload.map((fsr) => fsr.layer.id) |         const layersToShow = payload.map((fsr) => fsr.layer.id) | ||||||
|         console.log("Layers to show are", layersToShow) |  | ||||||
|         for (const otherLayer of state.layerState.filteredLayers.values()) { |         for (const otherLayer of state.layerState.filteredLayers.values()) { | ||||||
|             const layer = otherLayer.layerDef |             const layer = otherLayer.layerDef | ||||||
|             if (!layer.isNormal()) { |             if (!layer.isNormal()) { | ||||||
|  | @ -167,4 +167,45 @@ export default class SearchState { | ||||||
|         this.state.featureProperties.trackFeature(f) |         this.state.featureProperties.trackFeature(f) | ||||||
|         this.state.selectedElement.set(f) |         this.state.selectedElement.set(f) | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|  |     public moveToBestMatch() { | ||||||
|  |         const suggestion = this.suggestions.data?.[0] | ||||||
|  |         if (suggestion) { | ||||||
|  |             this.applyGeocodeResult(suggestion) | ||||||
|  |         } | ||||||
|  |         if (this.suggestionsSearchRunning.data) { | ||||||
|  |             this.suggestionsSearchRunning.addCallback(() => { | ||||||
|  |                 this.applyGeocodeResult(this.suggestions.data?.[0]) | ||||||
|  |                 return true // unregister
 | ||||||
|  |             }) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     applyGeocodeResult(entry: GeocodeResult) { | ||||||
|  |         if (!entry) { | ||||||
|  |             console.error("ApplyGeocodeResult got undefined/null") | ||||||
|  |         } | ||||||
|  |         console.log("Moving to", entry.description) | ||||||
|  |         const state = this.state | ||||||
|  |         if (entry.boundingbox) { | ||||||
|  |             const [lat0, lat1, lon0, lon1] = entry.boundingbox | ||||||
|  |             state.mapProperties.bounds.set( | ||||||
|  |                 new BBox([ | ||||||
|  |                     [lon0, lat0], | ||||||
|  |                     [lon1, lat1] | ||||||
|  |                 ]).pad(0.01) | ||||||
|  |             ) | ||||||
|  |         } else { | ||||||
|  |             state.mapProperties.flyTo( | ||||||
|  |                 entry.lon, | ||||||
|  |                 entry.lat, | ||||||
|  |                 GeocodingUtils.categoryToZoomLevel[entry.category] ?? 17 | ||||||
|  |             ) | ||||||
|  |         } | ||||||
|  |         if (entry.feature?.properties?.id) { | ||||||
|  |             state.selectedElement.set(entry.feature) | ||||||
|  |         } | ||||||
|  |         state.userRelatedState.recentlyVisitedSearch.add(entry) | ||||||
|  |         this.closeIfFullscreen() | ||||||
|  |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -5,17 +5,14 @@ | ||||||
|   import LayerConfig from "../../Models/ThemeConfig/LayerConfig" |   import LayerConfig from "../../Models/ThemeConfig/LayerConfig" | ||||||
|   import { UIEventSource } from "../../Logic/UIEventSource" |   import { UIEventSource } from "../../Logic/UIEventSource" | ||||||
|   import TagRenderingConfig from "../../Models/ThemeConfig/TagRenderingConfig" |   import TagRenderingConfig from "../../Models/ThemeConfig/TagRenderingConfig" | ||||||
|   import { createEventDispatcher } from "svelte" |  | ||||||
|   import type { SpecialVisualizationState } from "../SpecialVisualization" |  | ||||||
|   import { BBox } from "../../Logic/BBox" |  | ||||||
|   import ToSvelte from "../Base/ToSvelte.svelte" |  | ||||||
|   import Icon from "../Map/Icon.svelte" |   import Icon from "../Map/Icon.svelte" | ||||||
|   import TagRenderingAnswer from "../Popup/TagRendering/TagRenderingAnswer.svelte" |   import TagRenderingAnswer from "../Popup/TagRendering/TagRenderingAnswer.svelte" | ||||||
|   import ArrowUp from "@babeard/svelte-heroicons/mini/ArrowUp" |   import ArrowUp from "@babeard/svelte-heroicons/mini/ArrowUp" | ||||||
|   import DefaultIcon from "../Map/DefaultIcon.svelte" |   import DefaultIcon from "../Map/DefaultIcon.svelte" | ||||||
|  |   import { WithSearchState } from "../../Models/ThemeViewState/WithSearchState" | ||||||
| 
 | 
 | ||||||
|   export let entry: GeocodeResult |   export let entry: GeocodeResult | ||||||
|   export let state: SpecialVisualizationState |   export let state: WithSearchState | ||||||
| 
 | 
 | ||||||
|   let layer: LayerConfig |   let layer: LayerConfig | ||||||
|   let tags: UIEventSource<Record<string, string>> |   let tags: UIEventSource<Record<string, string>> | ||||||
|  | @ -36,34 +33,15 @@ | ||||||
|   let inView = state.mapProperties.bounds.mapD((bounds) => bounds.contains([entry.lon, entry.lat])) |   let inView = state.mapProperties.bounds.mapD((bounds) => bounds.contains([entry.lon, entry.lat])) | ||||||
| 
 | 
 | ||||||
|   function select() { |   function select() { | ||||||
|     if (entry.boundingbox) { |     state.searchState.applyGeocodeResult(entry) | ||||||
|       const [lat0, lat1, lon0, lon1] = entry.boundingbox |  | ||||||
|       state.mapProperties.bounds.set( |  | ||||||
|         new BBox([ |  | ||||||
|           [lon0, lat0], |  | ||||||
|           [lon1, lat1], |  | ||||||
|         ]).pad(0.01) |  | ||||||
|       ) |  | ||||||
|     } else { |  | ||||||
|       state.mapProperties.flyTo( |  | ||||||
|         entry.lon, |  | ||||||
|         entry.lat, |  | ||||||
|         GeocodingUtils.categoryToZoomLevel[entry.category] ?? 17 |  | ||||||
|       ) |  | ||||||
|     } |  | ||||||
|     if (entry.feature?.properties?.id) { |  | ||||||
|       state.selectedElement.set(entry.feature) |  | ||||||
|     } |  | ||||||
|     state.userRelatedState.recentlyVisitedSearch.add(entry) |  | ||||||
|     state.searchState.closeIfFullscreen() |  | ||||||
|   } |   } | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <button class="unstyled link-no-underline searchresult w-full" on:click={() => select()}> | <button class="unstyled link-no-underline searchresult w-full" on:click={() => select()}> | ||||||
|   <div class="flex w-full items-center gap-y-2 p-2"> |   <div class="flex w-full items-center gap-y-2 p-2"> | ||||||
|     {#if layer} |     {#if layer} | ||||||
|       <div class="h-6"> |       <div class="h-6 w-6"> | ||||||
|         <DefaultIcon {layer} properties={entry.feature.properties} clss="w-6 h-6" /> |         <DefaultIcon {layer} properties={entry.feature.properties} /> | ||||||
|       </div> |       </div> | ||||||
|     {:else if entry.category} |     {:else if entry.category} | ||||||
|       <Icon |       <Icon | ||||||
|  |  | ||||||
|  | @ -367,6 +367,7 @@ | ||||||
|         <div class="flex flex-grow items-center justify-end"> |         <div class="flex flex-grow items-center justify-end"> | ||||||
|           <div class="w-full sm:w-64"> |           <div class="w-full sm:w-64"> | ||||||
|             <Searchbar |             <Searchbar | ||||||
|  |               on:search={() => state.searchState.moveToBestMatch()} | ||||||
|               value={state.searchState.searchTerm} |               value={state.searchState.searchTerm} | ||||||
|               isFocused={state.searchState.searchIsFocused} |               isFocused={state.searchState.searchIsFocused} | ||||||
|             /> |             /> | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue