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