forked from MapComplete/MapComplete
190 lines
7.5 KiB
TypeScript
190 lines
7.5 KiB
TypeScript
|
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)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
|
||
|
}
|