MapComplete/src/Logic/Search/LocalElementSearch.ts

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

125 lines
4.9 KiB
TypeScript
Raw Normal View History

2024-08-26 13:09:46 +02:00
import GeocodingProvider, { SearchResult, GeocodingOptions } from "./GeocodingProvider"
2024-08-15 01:51:33 +02:00
import ThemeViewState from "../../Models/ThemeViewState"
import { Utils } from "../../Utils"
import { Feature } from "geojson"
import { GeoOperations } from "../GeoOperations"
2024-08-22 22:50:37 +02:00
import { ImmutableStore, Store, Stores } from "../UIEventSource"
import OpenStreetMapIdSearch from "./OpenStreetMapIdSearch"
2024-08-15 01:51:33 +02:00
2024-08-22 22:50:37 +02:00
type IntermediateResult = {
feature: Feature,
/**
* Lon, lat
*/
center: [number, number],
levehnsteinD: number,
physicalDistance: number,
searchTerms: string[],
description: string
}
2024-08-15 01:51:33 +02:00
export default class LocalElementSearch implements GeocodingProvider {
private readonly _state: ThemeViewState
2024-08-21 14:06:42 +02:00
private readonly _limit: number
2024-08-15 01:51:33 +02:00
2024-08-21 14:06:42 +02:00
constructor(state: ThemeViewState, limit: number) {
2024-08-15 01:51:33 +02:00
this._state = state
2024-08-21 14:06:42 +02:00
this._limit = limit
2024-08-15 01:51:33 +02:00
}
2024-08-26 13:09:46 +02:00
async search(query: string, options?: GeocodingOptions): Promise<SearchResult[]> {
2024-08-22 22:50:37 +02:00
return this.searchEntries(query, options, false).data
2024-08-15 01:51:33 +02:00
}
private getPartialResult(query: string, candidateId: string | undefined, matchStart: boolean, centerpoint: [number, number], features: Feature[]): IntermediateResult[] {
2024-08-22 22:50:37 +02:00
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])
let levehnsteinD: number
if (candidateId === props.id) {
levehnsteinD = 0
} else {
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)
}))
}
2024-08-22 22:50:37 +02:00
const center = GeoOperations.centerpointCoordinates(feature)
2024-08-26 13:09:46 +02:00
if ((levehnsteinD / query.length) <= 0.3) {
2024-08-22 22:50:37 +02:00
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,
2024-08-22 22:50:37 +02:00
})
}
}
return results
}
2024-08-26 13:09:46 +02:00
searchEntries(query: string, options?: GeocodingOptions, matchStart?: boolean): Store<SearchResult[]> {
2024-08-15 01:51:33 +02:00
if (query.length < 3) {
2024-08-22 22:50:37 +02:00
return new ImmutableStore([])
2024-08-15 01:51:33 +02:00
}
const center: { lon: number; lat: number } = this._state.mapProperties.location.data
const centerPoint: [number, number] = [center.lon, center.lat]
const properties = this._state.perLayer
const candidateId = OpenStreetMapIdSearch.extractId(query)
2024-08-15 01:51:33 +02:00
query = Utils.simplifyStringForSearch(query)
2024-08-22 22:50:37 +02:00
const partials: Store<IntermediateResult[]>[] = []
2024-08-15 01:51:33 +02:00
for (const [_, geoIndexedStore] of properties) {
const partialResult = geoIndexedStore.features.map(features => this.getPartialResult(query, candidateId, matchStart, centerPoint, features))
2024-08-22 22:50:37 +02:00
partials.push(partialResult)
2024-08-15 01:51:33 +02:00
}
2024-08-22 22:50:37 +02:00
const listed: Store<IntermediateResult[]> = Stores.concat(partials).map(l => l.flatMap(x => x))
2024-08-22 22:50:37 +02:00
return listed.mapD(results => {
results.sort((a, b) => (a.physicalDistance + a.levehnsteinD * 25) - (b.physicalDistance + b.levehnsteinD * 25))
if (this._limit) {
results = results.slice(0, this._limit)
2024-08-15 01:51:33 +02:00
}
2024-08-22 22:50:37 +02:00
return results.map(entry => {
const [osm_type, osm_id] = entry.feature.properties.id.split("/")
2024-08-26 13:09:46 +02:00
return <SearchResult>{
2024-08-22 22:50:37 +02:00
lon: entry.center[0],
lat: entry.center[1],
osm_type,
osm_id,
2024-08-22 22:50:37 +02:00
display_name: entry.searchTerms[0],
source: "localElementSearch",
feature: entry.feature,
importance: 1,
description: entry.description,
2024-08-22 22:50:37 +02:00
}
})
2024-08-15 01:51:33 +02:00
})
2024-08-22 22:50:37 +02:00
2024-08-15 01:51:33 +02:00
}
2024-08-26 13:09:46 +02:00
suggest(query: string, options?: GeocodingOptions): Store<SearchResult[]> {
2024-08-15 01:51:33 +02:00
return this.searchEntries(query, options, true)
}
}