More search functionality

This commit is contained in:
Pieter Vander Vennet 2024-08-22 22:50:37 +02:00
parent 5d0de8520b
commit 1c46a65c84
25 changed files with 962 additions and 846 deletions

View file

@ -1,5 +1,6 @@
import GeocodingProvider, { GeoCodeResult, GeocodingOptions } from "./GeocodingProvider"
import { Utils } from "../../Utils"
import { Store, Stores } from "../UIEventSource"
export default class CombinedSearcher implements GeocodingProvider {
private _providers: ReadonlyArray<GeocodingProvider>
@ -16,13 +17,13 @@ export default class CombinedSearcher implements GeocodingProvider {
* @param geocoded
* @private
*/
private merge(geocoded: GeoCodeResult[][]): GeoCodeResult[]{
const results : GeoCodeResult[] = []
private merge(geocoded: GeoCodeResult[][]): GeoCodeResult[] {
const results: GeoCodeResult[] = []
const seenIds = new Set<string>()
for (const geocodedElement of geocoded) {
for (const entry of geocodedElement) {
const id = entry.osm_type+ entry.osm_id
if(seenIds.has(id)){
const id = entry.osm_type + entry.osm_id
if (seenIds.has(id)) {
continue
}
seenIds.add(id)
@ -33,12 +34,12 @@ export default class CombinedSearcher implements GeocodingProvider {
}
async search(query: string, options?: GeocodingOptions): Promise<GeoCodeResult[]> {
const results = await Promise.all(this._providers.map(pr => pr.search(query, options)))
return this.merge(results)
const results = (await Promise.all(this._providers.map(pr => pr.search(query, options))))
return results.flatMap(x => x)
}
async suggest(query: string, options?: GeocodingOptions): Promise<GeoCodeResult[]> {
const results = await Promise.all(this._providersWithSuggest.map(pr => pr.suggest(query, options)))
return this.merge(results)
suggest(query: string, options?: GeocodingOptions): Store<GeoCodeResult[]> {
return Stores.concat(this._providersWithSuggest.map(pr => pr.suggest(query, options)))
}
}

View file

@ -1,5 +1,6 @@
import GeocodingProvider, { GeoCodeResult, GeocodingOptions } from "./GeocodingProvider"
import { Utils } from "../../Utils"
import { ImmutableStore, Store } from "../UIEventSource"
/**
* A simple search-class which interprets possible locations
@ -17,28 +18,25 @@ export default class CoordinateSearch implements GeocodingProvider {
]
/**
*
* @param query
* @param options
*
* const ls = new CoordinateSearch()
* const results = await ls.search("https://www.openstreetmap.org/search?query=Brugge#map=11/51.2611/3.2217")
* const results = ls.directSearch("https://www.openstreetmap.org/search?query=Brugge#map=11/51.2611/3.2217")
* results.length // => 1
* results[0] // => {lat: 51.2611, lon: 3.2217, display_name: "lon: 3.2217, lat: 51.2611", "category": "coordinate","source": "coordinateSearch"}
*
* const ls = new CoordinateSearch()
* const results = await ls.search("https://www.openstreetmap.org/#map=11/51.2611/3.2217")
* const results = ls.directSearch("https://www.openstreetmap.org/#map=11/51.2611/3.2217")
* results.length // => 1
* results[0] // => {lat: 51.2611, lon: 3.2217, display_name: "lon: 3.2217, lat: 51.2611", "category": "coordinate","source": "coordinateSearch"}
*
* const ls = new CoordinateSearch()
* const results = await ls.search("51.2611 3.2217")
* const results = ls.directSearch("51.2611 3.2217")
* results.length // => 2
* results[0] // => {lat: 51.2611, lon: 3.2217, display_name: "lon: 3.2217, lat: 51.2611", "category": "coordinate", "source": "coordinateSearch"}
* results[1] // => {lon: 51.2611, lat: 3.2217, display_name: "lon: 51.2611, lat: 3.2217", "category": "coordinate", "source": "coordinateSearch"}
*
*/
async search(query: string, options?: GeocodingOptions): Promise<GeoCodeResult[]> {
private directSearch(query: string): GeoCodeResult[] {
const matches = Utils.NoNull(CoordinateSearch.latLonRegexes.map(r => query.match(r))).map(m => <GeoCodeResult>{
lat: Number(m[1]),
@ -49,8 +47,7 @@ export default class CoordinateSearch implements GeocodingProvider {
})
const matchesLonLat = Utils.NoNull(CoordinateSearch.lonLatRegexes.map(r => query.match(r)))
const matchesLonLat = Utils.NoNull(CoordinateSearch.lonLatRegexes.map(r => query.match(r)))
.map(m => <GeoCodeResult>{
lat: Number(m[2]),
lon: Number(m[1]),
@ -58,12 +55,15 @@ export default class CoordinateSearch implements GeocodingProvider {
source: "coordinateSearch",
category: "coordinate"
})
return matches.concat(matchesLonLat)
}
suggest(query: string, options?: GeocodingOptions): Promise<GeoCodeResult[]> {
return this.search(query, options)
suggest(query: string): Store<GeoCodeResult[]> {
return new ImmutableStore(this.directSearch(query))
}
async search (query: string): Promise<GeoCodeResult[]> {
return this.directSearch(query)
}
}

View file

@ -1,6 +1,7 @@
import { BBox } from "../BBox"
import { Feature, Geometry } from "geojson"
import { DefaultPinIcon } from "../../Models/Constants"
import { Store } from "../UIEventSource"
export type GeocodingCategory = "coordinate" | "city" | "house" | "street" | "locality" | "country" | "train_station" | "county" | "airport"
@ -42,7 +43,7 @@ export default interface GeocodingProvider {
* @param query
* @param options
*/
suggest?(query: string, options?: GeocodingOptions): Promise<GeoCodeResult[]>
suggest?(query: string, options?: GeocodingOptions): Store<GeoCodeResult[]>
}
export type ReverseGeocodingResult = Feature<Geometry,{

View file

@ -3,7 +3,19 @@ import ThemeViewState from "../../Models/ThemeViewState"
import { Utils } from "../../Utils"
import { Feature } from "geojson"
import { GeoOperations } from "../GeoOperations"
import { ImmutableStore, Store, Stores } from "../UIEventSource"
type IntermediateResult = {
feature: Feature,
/**
* Lon, lat
*/
center: [number, number],
levehnsteinD: number,
physicalDistance: number,
searchTerms: string[],
description: string
}
export default class LocalElementSearch implements GeocodingProvider {
private readonly _state: ThemeViewState
private readonly _limit: number
@ -15,86 +27,91 @@ export default class LocalElementSearch implements GeocodingProvider {
}
async search(query: string, options?: GeocodingOptions): Promise<GeoCodeResult[]> {
return this.searchEntries(query, options, false)
return this.searchEntries(query, options, false).data
}
searchEntries(query: string, options?: GeocodingOptions, matchStart?: boolean): GeoCodeResult[] {
private getPartialResult(query: string, matchStart: boolean, centerpoint: [number, number], features: Feature[]): IntermediateResult[] {
const results: IntermediateResult [] = []
for (const feature of features) {
const props = feature.properties
const searchTerms: string[] = Utils.NoNull([props.name, props.alt_name, props.local_name,
(props["addr:street"] && props["addr:number"]) ?
props["addr:street"] + props["addr:number"] : undefined])
const levehnsteinD = Math.min(...searchTerms.flatMap(entry => entry.split(/ /)).map(entry => {
let simplified = Utils.simplifyStringForSearch(entry)
if (matchStart) {
simplified = simplified.slice(0, query.length)
}
return Utils.levenshteinDistance(query, simplified)
}))
const center = GeoOperations.centerpointCoordinates(feature)
if (levehnsteinD <= 2) {
let description = ""
if (feature.properties["addr:street"]) {
description += "" + feature.properties["addr:street"]
}
if (feature.properties["addr:housenumber"]) {
description += " " + feature.properties["addr:housenumber"]
}
results.push({
feature,
center,
physicalDistance: GeoOperations.distanceBetween(centerpoint, center),
levehnsteinD,
searchTerms,
description: description !== "" ? description : undefined
})
}
}
return results
}
searchEntries(query: string, options?: GeocodingOptions, matchStart?: boolean): Store<GeoCodeResult[]> {
if (query.length < 3) {
return []
return new ImmutableStore([])
}
const center: { lon: number; lat: number } = this._state.mapProperties.location.data
const centerPoint: [number, number] = [center.lon, center.lat]
let results: {
feature: Feature,
/**
* Lon, lat
*/
center: [number, number],
levehnsteinD: number,
physicalDistance: number,
searchTerms: string[],
description: string
}[] = []
const properties = this._state.perLayer
query = Utils.simplifyStringForSearch(query)
const partials: Store<IntermediateResult[]>[] = []
for (const [_, geoIndexedStore] of properties) {
for (const feature of geoIndexedStore.features.data) {
const props = feature.properties
const searchTerms: string[] = Utils.NoNull([props.name, props.alt_name, props.local_name,
(props["addr:street"] && props["addr:number"]) ?
props["addr:street"] + props["addr:number"] : undefined])
const partialResult = geoIndexedStore.features.map(features => this.getPartialResult(query, matchStart, centerPoint, features))
partials.push(partialResult)
}
const levehnsteinD = Math.min(...searchTerms.flatMap(entry => entry.split(/ /)).map(entry => {
let simplified = Utils.simplifyStringForSearch(entry)
if (matchStart) {
simplified = simplified.slice(0, query.length)
}
return Utils.levenshteinDistance(query, simplified)
}))
const center = GeoOperations.centerpointCoordinates(feature)
if (levehnsteinD <= 2) {
let description = ""
function ifDef(prefix: string, key: string){
if(feature.properties[key]){
description += prefix+ feature.properties[key]
}
}
ifDef("", "addr:street")
ifDef(" ", "addr:housenumber")
results.push({
feature,
center,
physicalDistance: GeoOperations.distanceBetween(centerPoint, center),
levehnsteinD,
searchTerms,
description: description !== "" ? description : undefined
})
const listed: Store<IntermediateResult[]> = Stores.concat(partials)
return listed.mapD(results => {
results.sort((a, b) => (a.physicalDistance + a.levehnsteinD * 25) - (b.physicalDistance + b.levehnsteinD * 25))
if (this._limit || options?.limit) {
results = results.slice(0, Math.min(this._limit ?? options?.limit, options?.limit ?? this._limit))
}
return results.map(entry => {
const id = entry.feature.properties.id.split("/")
return <GeoCodeResult>{
lon: entry.center[0],
lat: entry.center[1],
osm_type: id[0],
osm_id: id[1],
display_name: entry.searchTerms[0],
source: "localElementSearch",
feature: entry.feature,
importance: 1,
description: entry.description
}
}
}
results.sort((a, b) => (a.physicalDistance + a.levehnsteinD * 25) - (b.physicalDistance + b.levehnsteinD * 25))
if (this._limit || options?.limit) {
results = results.slice(0, Math.min(this._limit ?? options?.limit, options?.limit ?? this._limit))
}
return results.map(entry => {
const id = entry.feature.properties.id.split("/")
return <GeoCodeResult>{
lon: entry.center[0],
lat: entry.center[1],
osm_type: id[0],
osm_id: id[1],
display_name: entry.searchTerms[0],
source: "localElementSearch",
feature: entry.feature,
importance: 1,
description: entry.description
}
})
})
}
async suggest(query: string, options?: GeocodingOptions): Promise<GeoCodeResult[]> {
suggest(query: string, options?: GeocodingOptions): Store<GeoCodeResult[]> {
return this.searchEntries(query, options, true)
}

View file

@ -3,9 +3,10 @@ import { BBox } from "../BBox"
import Constants from "../../Models/Constants"
import { FeatureCollection } from "geojson"
import Locale from "../../UI/i18n/Locale"
import GeocodingProvider, { GeoCodeResult, ReverseGeocodingProvider } from "./GeocodingProvider"
import GeocodingProvider, { GeoCodeResult } from "./GeocodingProvider"
import { Store, UIEventSource } from "../UIEventSource"
export class NominatimGeocoding implements GeocodingProvider, ReverseGeocodingProvider {
export class NominatimGeocoding implements GeocodingProvider {
private readonly _host ;
@ -13,14 +14,14 @@ export class NominatimGeocoding implements GeocodingProvider, ReverseGeocodingPr
this._host = host
}
public async search(query: string, options?: { bbox?: BBox; limit?: number }): Promise<GeoCodeResult[]> {
public search(query: string, options?: { bbox?: BBox; limit?: number }): Promise<GeoCodeResult[]> {
const b = options?.bbox ?? BBox.global
const url = `${
this._host
}search?format=json&limit=${options?.limit ?? 1}&viewbox=${b.getEast()},${b.getNorth()},${b.getWest()},${b.getSouth()}&accept-language=${
Locale.language.data
}&q=${query}`
return await Utils.downloadJson(url)
return Utils.downloadJson(url)
}

View file

@ -1,6 +1,7 @@
import Constants from "../../Models/Constants"
import GeocodingProvider, {
GeoCodeResult, GeocodingCategory,
GeoCodeResult,
GeocodingCategory,
GeocodingOptions,
ReverseGeocodingProvider,
ReverseGeocodingResult
@ -9,6 +10,7 @@ import { Utils } from "../../Utils"
import { Feature, FeatureCollection } from "geojson"
import Locale from "../../UI/i18n/Locale"
import { GeoOperations } from "../GeoOperations"
import { Store, Stores } from "../UIEventSource"
export default class PhotonSearch implements GeocodingProvider, ReverseGeocodingProvider {
private _endpoint: string
@ -52,8 +54,8 @@ export default class PhotonSearch implements GeocodingProvider, ReverseGeocoding
}
search(query: string, options?: GeocodingOptions): Promise<GeoCodeResult[]> {
return this.suggest(query, options)
suggest(query: string, options?: GeocodingOptions): Store<GeoCodeResult[]> {
return Stores.FromPromise(this.search(query, options))
}
private buildDescription(entry: Feature) {
@ -71,7 +73,7 @@ export default class PhotonSearch implements GeocodingProvider, ReverseGeocoding
case "house": {
const addr = ifdef("", p.street) + ifdef(" ", p.housenumber)
if(!addr){
if (!addr) {
return p.city
}
return addr + ifdef(", ", p.city)
@ -81,8 +83,8 @@ export default class PhotonSearch implements GeocodingProvider, ReverseGeocoding
return p.city ?? p.country
case "city":
case "locality":
if(p.state){
return p.state + ifdef(", ", p.country)
if (p.state) {
return p.state + ifdef(", ", p.country)
}
return p.country
case "country":
@ -91,18 +93,18 @@ export default class PhotonSearch implements GeocodingProvider, ReverseGeocoding
}
private getCategory(entry: Feature){
private getCategory(entry: Feature) {
const p = entry.properties
if(p.osm_value === "train_station" || p.osm_key === "railway"){
if (p.osm_value === "train_station" || p.osm_key === "railway") {
return "train_station"
}
if(p.osm_value === "aerodrome" || p.osm_key === "aeroway"){
if (p.osm_value === "aerodrome" || p.osm_key === "aeroway") {
return "airport"
}
return p.type
}
async suggest?(query: string, options?: GeocodingOptions): Promise<GeoCodeResult[]> {
async search(query: string, options?: GeocodingOptions): Promise<GeoCodeResult[]> {
if (query.length < 3) {
return []
}

View file

@ -18,12 +18,15 @@ export class RecentSearch {
state.selectedElement.addCallbackAndRunD(selected => {
const [osm_type, osm_id] = selected.properties.id.split("/")
if(!osm_id){
return
}
const [lon, lat] = GeoOperations.centerpointCoordinates(selected)
const entry = <GeoCodeResult>{
feature: selected,
osm_id, osm_type,
description: "Viewed recently",
lon, lat
}
this.addSelected(entry)

View file

@ -4,7 +4,7 @@ import { MinimalLayoutInformation } from "../../Models/ThemeConfig/LayoutConfig"
import { SpecialVisualizationState } from "../../UI/SpecialVisualization"
import { Utils } from "../../Utils"
import MoreScreen from "../../UI/BigComponents/MoreScreen"
import { Store } from "../UIEventSource"
import { ImmutableStore, Store } from "../UIEventSource"
export default class ThemeSearch implements GeocodingProvider {
@ -17,11 +17,15 @@ export default class ThemeSearch implements GeocodingProvider {
this._knownHiddenThemes = MoreScreen.knownHiddenThemes(this._state.osmConnection)
}
search(query: string, options?: GeocodingOptions): Promise<GeoCodeResult[]> {
return this.suggest(query, options)
async search(query: string, options?: GeocodingOptions): Promise<GeoCodeResult[]> {
return this.searchDirect(query, options)
}
async suggest?(query: string, options?: GeocodingOptions): Promise<GeoCodeResult[]> {
suggest(query: string, options?: GeocodingOptions): Store<GeoCodeResult[]> {
return new ImmutableStore(this.searchDirect(query, options))
}
private searchDirect(query: string, options?: GeocodingOptions): GeoCodeResult[] {
if(query.length < 1){
return []
}
@ -33,10 +37,10 @@ export default class ThemeSearch implements GeocodingProvider {
.filter(th => MoreScreen.MatchesLayout(th, query))
.slice(0, limit + 1)
return withMatch.map(match => (<GeoCodeResult> {
return withMatch.map(match => <GeoCodeResult> {
payload: match,
osm_id: match.id
}))
})
}