First search with suggestions
This commit is contained in:
parent
874f82be82
commit
3cd04df60b
37 changed files with 677 additions and 85 deletions
|
@ -28,10 +28,10 @@ export class SummaryTileSourceRewriter implements FeatureSource {
|
|||
!l.layerDef.id.startsWith("note_import")
|
||||
)
|
||||
this._summarySource = summarySource
|
||||
filteredLayers.forEach((v, k) => {
|
||||
v.isDisplayed.addCallback((_) => this.update())
|
||||
filteredLayers.forEach((v) => {
|
||||
v.isDisplayed.addCallback(() => this.update())
|
||||
})
|
||||
this._summarySource.features.addCallbackAndRunD((_) => this.update())
|
||||
this._summarySource.features.addCallbackAndRunD(() => this.update())
|
||||
}
|
||||
|
||||
private update() {
|
||||
|
@ -78,6 +78,9 @@ export class SummaryTileSource extends DynamicTileSource {
|
|||
isActive?: Store<boolean>
|
||||
}
|
||||
) {
|
||||
if(layers.length === 0){
|
||||
return
|
||||
}
|
||||
const layersSummed = layers.join("+")
|
||||
const zDiff = 2
|
||||
super(
|
||||
|
|
21
src/Logic/Geocoding/CombinedSearcher.ts
Normal file
21
src/Logic/Geocoding/CombinedSearcher.ts
Normal file
|
@ -0,0 +1,21 @@
|
|||
import GeocodingProvider, { GeoCodeResult, GeocodingOptions } from "./GeocodingProvider"
|
||||
|
||||
export default class CombinedSearcher implements GeocodingProvider {
|
||||
private _providers: ReadonlyArray<GeocodingProvider>
|
||||
private _providersWithSuggest: ReadonlyArray<GeocodingProvider>
|
||||
|
||||
constructor(...providers: ReadonlyArray<GeocodingProvider>) {
|
||||
this._providers = providers
|
||||
this._providersWithSuggest = providers.filter(pr => pr.suggest !== undefined)
|
||||
}
|
||||
|
||||
async search(query: string, options?: GeocodingOptions): Promise<GeoCodeResult[]> {
|
||||
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 results.flatMap(x => x)
|
||||
}
|
||||
}
|
67
src/Logic/Geocoding/CoordinateSearch.ts
Normal file
67
src/Logic/Geocoding/CoordinateSearch.ts
Normal file
|
@ -0,0 +1,67 @@
|
|||
import GeocodingProvider, { GeoCodeResult, GeocodingOptions } from "./GeocodingProvider"
|
||||
import { Utils } from "../../Utils"
|
||||
|
||||
/**
|
||||
* A simple search-class which interprets possible locations
|
||||
*/
|
||||
export default class CoordinateSearch implements GeocodingProvider {
|
||||
private static readonly latLonRegexes: ReadonlyArray<RegExp> = [
|
||||
/([0-9]+\.[0-9]+)[ ,;]+([0-9]+\.[0-9]+)/,
|
||||
/lat:?[ ]*([0-9]+\.[0-9]+)[ ,;]+lon:?[ ]*([0-9]+\.[0-9]+)/,
|
||||
/https:\/\/www.openstreetmap.org\/.*#map=[0-9]+\/([0-9]+\.[0-9]+)\/([0-9]+\.[0-9]+)/,
|
||||
/https:\/\/www.google.com\/maps\/@([0-9]+.[0-9]+),([0-9]+.[0-9]+).*/
|
||||
]
|
||||
|
||||
private static readonly lonLatRegexes: ReadonlyArray<RegExp> = [
|
||||
/([0-9]+\.[0-9]+)[ ,;]+([0-9]+\.[0-9]+)/
|
||||
]
|
||||
|
||||
/**
|
||||
*
|
||||
* @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")
|
||||
* results.length // => 1
|
||||
* results[0] // => {lat: 51.2611, lon: 3.2217, display_name: "lon: 3.2217, lat: 51.2611"}
|
||||
*
|
||||
* const ls = new CoordinateSearch()
|
||||
* const results = await ls.search("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"}
|
||||
*
|
||||
* const ls = new CoordinateSearch()
|
||||
* const results = await ls.search("51.2611 3.2217")
|
||||
* results.length // => 2
|
||||
* results[0] // => {lat: 51.2611, lon: 3.2217, display_name: "lon: 3.2217, lat: 51.2611"}
|
||||
* results[1] // => {lon: 51.2611, lat: 3.2217, display_name: "lon: 51.2611, lat: 3.2217"}
|
||||
*
|
||||
*/
|
||||
async search(query: string, options?: GeocodingOptions): Promise<GeoCodeResult[]> {
|
||||
|
||||
const matches = Utils.NoNull(CoordinateSearch.latLonRegexes.map(r => query.match(r))).map(m => <GeoCodeResult>{
|
||||
lat: Number(m[1]),
|
||||
lon: Number(m[2]),
|
||||
display_name: "lon: " + m[2] + ", lat: " + m[1],
|
||||
source: "coordinateSearch"
|
||||
})
|
||||
|
||||
|
||||
|
||||
const matchesLonLat = Utils.NoNull(CoordinateSearch.lonLatRegexes.map(r => query.match(r)))
|
||||
.map(m => <GeoCodeResult>{
|
||||
lat: Number(m[2]),
|
||||
lon: Number(m[1]),
|
||||
display_name: "lon: " + m[1] + ", lat: " + m[2],
|
||||
source: "coordinateSearch"
|
||||
})
|
||||
|
||||
return matches.concat(matchesLonLat)
|
||||
}
|
||||
|
||||
suggest(query: string, options?: GeocodingOptions): Promise<GeoCodeResult[]> {
|
||||
return this.search(query, options)
|
||||
}
|
||||
|
||||
}
|
43
src/Logic/Geocoding/GeocodingProvider.ts
Normal file
43
src/Logic/Geocoding/GeocodingProvider.ts
Normal file
|
@ -0,0 +1,43 @@
|
|||
import { BBox } from "../BBox"
|
||||
import { Feature, FeatureCollection } from "geojson"
|
||||
|
||||
export type GeoCodeResult = {
|
||||
display_name: string
|
||||
feature?: Feature,
|
||||
lat: number
|
||||
lon: number
|
||||
/**
|
||||
* Format:
|
||||
* [lat, lat, lon, lon]
|
||||
*/
|
||||
boundingbox?: number[]
|
||||
osm_type?: "node" | "way" | "relation"
|
||||
osm_id?: string
|
||||
}
|
||||
|
||||
export interface GeocodingOptions {
|
||||
bbox?: BBox,
|
||||
limit?: number
|
||||
}
|
||||
|
||||
|
||||
export default interface GeocodingProvider {
|
||||
|
||||
|
||||
search(query: string, options?: GeocodingOptions): Promise<GeoCodeResult[]>
|
||||
|
||||
/**
|
||||
* @param query
|
||||
* @param options
|
||||
*/
|
||||
suggest?(query: string, options?: GeocodingOptions): Promise<GeoCodeResult[]>
|
||||
}
|
||||
|
||||
export interface ReverseGeocodingProvider {
|
||||
reverseSearch(
|
||||
coordinate: { lon: number; lat: number },
|
||||
zoom: number,
|
||||
language?: string
|
||||
): Promise<FeatureCollection> ;
|
||||
}
|
||||
|
86
src/Logic/Geocoding/LocalElementSearch.ts
Normal file
86
src/Logic/Geocoding/LocalElementSearch.ts
Normal file
|
@ -0,0 +1,86 @@
|
|||
import GeocodingProvider, { GeoCodeResult, GeocodingOptions } from "./GeocodingProvider"
|
||||
import ThemeViewState from "../../Models/ThemeViewState"
|
||||
import { Utils } from "../../Utils"
|
||||
import { Feature } from "geojson"
|
||||
import { GeoOperations } from "../GeoOperations"
|
||||
|
||||
export default class LocalElementSearch implements GeocodingProvider {
|
||||
private readonly _state: ThemeViewState
|
||||
|
||||
constructor(state: ThemeViewState) {
|
||||
this._state = state
|
||||
|
||||
}
|
||||
|
||||
async search(query: string, options?: GeocodingOptions): Promise<GeoCodeResult[]> {
|
||||
return this.searchEntries(query, options, false)
|
||||
}
|
||||
|
||||
searchEntries(query: string, options?: GeocodingOptions, matchStart?: boolean): GeoCodeResult[] {
|
||||
if (query.length < 3) {
|
||||
return []
|
||||
}
|
||||
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[]
|
||||
}[] = []
|
||||
const properties = this._state.perLayer
|
||||
query = Utils.simplifyStringForSearch(query)
|
||||
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 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) {
|
||||
results.push({
|
||||
feature,
|
||||
center,
|
||||
physicalDistance: GeoOperations.distanceBetween(centerPoint, center),
|
||||
levehnsteinD,
|
||||
searchTerms
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
results.sort((a, b) => (a.physicalDistance + a.levehnsteinD * 25) - (b.physicalDistance + b.levehnsteinD * 25))
|
||||
if (options?.limit) {
|
||||
results = results.slice(0, options.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
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async suggest(query: string, options?: GeocodingOptions): Promise<GeoCodeResult[]> {
|
||||
return this.searchEntries(query, options, true)
|
||||
}
|
||||
|
||||
}
|
39
src/Logic/Geocoding/NominatimGeocoding.ts
Normal file
39
src/Logic/Geocoding/NominatimGeocoding.ts
Normal file
|
@ -0,0 +1,39 @@
|
|||
import { Utils } from "../../Utils"
|
||||
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"
|
||||
|
||||
export class NominatimGeocoding implements GeocodingProvider, ReverseGeocodingProvider {
|
||||
|
||||
private readonly _host ;
|
||||
|
||||
constructor(host: string = Constants.nominatimEndpoint) {
|
||||
this._host = host
|
||||
}
|
||||
|
||||
public async 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)
|
||||
}
|
||||
|
||||
|
||||
async reverseSearch(
|
||||
coordinate: { lon: number; lat: number },
|
||||
zoom: number = 17,
|
||||
language?: string
|
||||
): Promise<FeatureCollection> {
|
||||
// https://nominatim.org/release-docs/develop/api/Reverse/
|
||||
// IF the zoom is low, it'll only return a country instead of an address
|
||||
const url = `${this._host}reverse?format=geojson&lat=${coordinate.lat}&lon=${
|
||||
coordinate.lon
|
||||
}&zoom=${Math.ceil(zoom) + 1}&accept-language=${language}`
|
||||
return Utils.downloadJson(url)
|
||||
}
|
||||
}
|
|
@ -1,45 +0,0 @@
|
|||
import { Utils } from "../../Utils"
|
||||
import { BBox } from "../BBox"
|
||||
import Constants from "../../Models/Constants"
|
||||
import { FeatureCollection } from "geojson"
|
||||
import Locale from "../../UI/i18n/Locale"
|
||||
|
||||
export interface GeoCodeResult {
|
||||
display_name: string
|
||||
lat: number
|
||||
lon: number
|
||||
/**
|
||||
* Format:
|
||||
* [lat, lat, lon, lon]
|
||||
*/
|
||||
boundingbox: number[]
|
||||
osm_type: "node" | "way" | "relation"
|
||||
osm_id: string
|
||||
}
|
||||
|
||||
export class Geocoding {
|
||||
public static readonly host = Constants.nominatimEndpoint
|
||||
|
||||
static async Search(query: string, bbox: BBox): Promise<GeoCodeResult[]> {
|
||||
const b = bbox ?? BBox.global
|
||||
const url = `${
|
||||
Geocoding.host
|
||||
}search?format=json&limit=1&viewbox=${b.getEast()},${b.getNorth()},${b.getWest()},${b.getSouth()}&accept-language=${
|
||||
Locale.language.data
|
||||
}&q=${query}`
|
||||
return Utils.downloadJson(url)
|
||||
}
|
||||
|
||||
static async reverse(
|
||||
coordinate: { lon: number; lat: number },
|
||||
zoom: number = 17,
|
||||
language?: string
|
||||
): Promise<FeatureCollection> {
|
||||
// https://nominatim.org/release-docs/develop/api/Reverse/
|
||||
// IF the zoom is low, it'll only return a country instead of an address
|
||||
const url = `${Geocoding.host}reverse?format=geojson&lat=${coordinate.lat}&lon=${
|
||||
coordinate.lon
|
||||
}&zoom=${Math.ceil(zoom) + 1}&accept-language=${language}`
|
||||
return Utils.downloadJson(url)
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue