forked from MapComplete/MapComplete
Search: add support for osm.org urls such as osm.org/node/42
This commit is contained in:
parent
3ac2f96868
commit
3ab1a0a3f2
11 changed files with 118 additions and 35 deletions
|
@ -40,6 +40,7 @@ export default class CombinedSearcher implements GeocodingProvider {
|
||||||
|
|
||||||
suggest(query: string, options?: GeocodingOptions): Store<GeoCodeResult[]> {
|
suggest(query: string, options?: GeocodingOptions): Store<GeoCodeResult[]> {
|
||||||
return Stores.concat(this._providersWithSuggest.map(pr => pr.suggest(query, options)))
|
return Stores.concat(this._providersWithSuggest.map(pr => pr.suggest(query, options)))
|
||||||
|
.map(gcrss => this.merge(gcrss))
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,30 +27,30 @@ export default class CoordinateSearch implements GeocodingProvider {
|
||||||
* const ls = new CoordinateSearch()
|
* const ls = new CoordinateSearch()
|
||||||
* const results = ls.directSearch("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.length // => 1
|
||||||
* results[0] // => {lat: 51.2611, lon: 3.2217, display_name: "lon: 3.2217, lat: 51.2611", "category": "coordinate","source": "coordinateSearch"}
|
* results[0] // => {lat: 51.2611, lon: 3.2217, display_name: "lon: 3.2217, lat: 51.2611", "category": "coordinate","source": "coordinate:latlon"}
|
||||||
*
|
*
|
||||||
* const ls = new CoordinateSearch()
|
* const ls = new CoordinateSearch()
|
||||||
* const results = ls.directSearch("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.length // => 1
|
||||||
* results[0] // => {lat: 51.2611, lon: 3.2217, display_name: "lon: 3.2217, lat: 51.2611", "category": "coordinate","source": "coordinateSearch"}
|
* results[0] // => {lat: 51.2611, lon: 3.2217, display_name: "lon: 3.2217, lat: 51.2611", "category": "coordinate","source": "coordinate:latlon"}
|
||||||
*
|
*
|
||||||
* const ls = new CoordinateSearch()
|
* const ls = new CoordinateSearch()
|
||||||
* const results = ls.directSearch("51.2611 3.2217")
|
* const results = ls.directSearch("51.2611 3.2217")
|
||||||
* results.length // => 2
|
* results.length // => 2
|
||||||
* results[0] // => {lat: 51.2611, lon: 3.2217, display_name: "lon: 3.2217, lat: 51.2611", "category": "coordinate", "source": "coordinateSearch"}
|
* results[0] // => {lat: 51.2611, lon: 3.2217, display_name: "lon: 3.2217, lat: 51.2611", "category": "coordinate", "source": "coordinate:latlon"}
|
||||||
* results[1] // => {lon: 51.2611, lat: 3.2217, display_name: "lon: 51.2611, lat: 3.2217", "category": "coordinate", "source": "coordinateSearch"}
|
* results[1] // => {lon: 51.2611, lat: 3.2217, display_name: "lon: 51.2611, lat: 3.2217", "category": "coordinate", "source": "coordinate:lonlat"}
|
||||||
*
|
*
|
||||||
* // test OSM-XML format
|
* // test OSM-XML format
|
||||||
* const ls = new CoordinateSearch()
|
* const ls = new CoordinateSearch()
|
||||||
* const results = ls.directSearch(' lat="57.5802905" lon="12.7202538"')
|
* const results = ls.directSearch(' lat="57.5802905" lon="12.7202538"')
|
||||||
* results.length // => 1
|
* results.length // => 1
|
||||||
* results[0] // => {lat: 57.5802905, lon: 12.7202538, display_name: "lon: 12.7202538, lat: 57.5802905", "category": "coordinate", "source": "coordinateSearch"}
|
* results[0] // => {lat: 57.5802905, lon: 12.7202538, display_name: "lon: 12.7202538, lat: 57.5802905", "category": "coordinate", "source": "coordinate:latlon"}
|
||||||
*
|
*
|
||||||
* // should work with negative coordinates
|
* // should work with negative coordinates
|
||||||
* const ls = new CoordinateSearch()
|
* const ls = new CoordinateSearch()
|
||||||
* const results = ls.directSearch(' lat="-57.5802905" lon="-12.7202538"')
|
* const results = ls.directSearch(' lat="-57.5802905" lon="-12.7202538"')
|
||||||
* results.length // => 1
|
* results.length // => 1
|
||||||
* results[0] // => {lat: -57.5802905, lon: -12.7202538, display_name: "lon: -12.7202538, lat: -57.5802905", "category": "coordinate", "source": "coordinateSearch"}
|
* results[0] // => {lat: -57.5802905, lon: -12.7202538, display_name: "lon: -12.7202538, lat: -57.5802905", "category": "coordinate", "source": "coordinate:latlon"}
|
||||||
*/
|
*/
|
||||||
private directSearch(query: string): GeoCodeResult[] {
|
private directSearch(query: string): GeoCodeResult[] {
|
||||||
|
|
||||||
|
@ -58,7 +58,7 @@ export default class CoordinateSearch implements GeocodingProvider {
|
||||||
lat: Number(m[1]),
|
lat: Number(m[1]),
|
||||||
lon: Number(m[2]),
|
lon: Number(m[2]),
|
||||||
display_name: "lon: " + m[2] + ", lat: " + m[1],
|
display_name: "lon: " + m[2] + ", lat: " + m[1],
|
||||||
source: "coordinateSearch",
|
source: "coordinate:latlon",
|
||||||
category: "coordinate"
|
category: "coordinate"
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -68,8 +68,8 @@ export default class CoordinateSearch implements GeocodingProvider {
|
||||||
lat: Number(m[2]),
|
lat: Number(m[2]),
|
||||||
lon: Number(m[1]),
|
lon: Number(m[1]),
|
||||||
display_name: "lon: " + m[1] + ", lat: " + m[2],
|
display_name: "lon: " + m[1] + ", lat: " + m[2],
|
||||||
source: "coordinateSearch",
|
category: "coordinate",
|
||||||
category: "coordinate"
|
source: "coordinate:lonlat"
|
||||||
})
|
})
|
||||||
return matches.concat(matchesLonLat)
|
return matches.concat(matchesLonLat)
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,7 +27,8 @@ export default class GeocodingFeatureSource implements FeatureSource {
|
||||||
display_name: gc.display_name,
|
display_name: gc.display_name,
|
||||||
osm_id: gc.osm_type + "/" + gc.osm_id,
|
osm_id: gc.osm_type + "/" + gc.osm_id,
|
||||||
osm_key: gc.feature?.properties?.osm_key,
|
osm_key: gc.feature?.properties?.osm_key,
|
||||||
osm_value: gc.feature?.properties?.osm_value
|
osm_value: gc.feature?.properties?.osm_value,
|
||||||
|
source: gc.source
|
||||||
},
|
},
|
||||||
geometry: {
|
geometry: {
|
||||||
type: "Point",
|
type: "Point",
|
||||||
|
|
|
@ -27,7 +27,8 @@ export type GeoCodeResult = {
|
||||||
osm_type?: "node" | "way" | "relation"
|
osm_type?: "node" | "way" | "relation"
|
||||||
osm_id?: string,
|
osm_id?: string,
|
||||||
category?: GeocodingCategory,
|
category?: GeocodingCategory,
|
||||||
payload?: object
|
payload?: object,
|
||||||
|
source?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GeocodingOptions {
|
export interface GeocodingOptions {
|
||||||
|
|
|
@ -4,6 +4,7 @@ import { Utils } from "../../Utils"
|
||||||
import { Feature } from "geojson"
|
import { Feature } from "geojson"
|
||||||
import { GeoOperations } from "../GeoOperations"
|
import { GeoOperations } from "../GeoOperations"
|
||||||
import { ImmutableStore, Store, Stores } from "../UIEventSource"
|
import { ImmutableStore, Store, Stores } from "../UIEventSource"
|
||||||
|
import OpenStreetMapIdSearch from "./OpenStreetMapIdSearch"
|
||||||
|
|
||||||
type IntermediateResult = {
|
type IntermediateResult = {
|
||||||
feature: Feature,
|
feature: Feature,
|
||||||
|
@ -30,7 +31,7 @@ export default class LocalElementSearch implements GeocodingProvider {
|
||||||
return this.searchEntries(query, options, false).data
|
return this.searchEntries(query, options, false).data
|
||||||
}
|
}
|
||||||
|
|
||||||
private getPartialResult(query: string, matchStart: boolean, centerpoint: [number, number], features: Feature[]): IntermediateResult[] {
|
private getPartialResult(query: string, candidateId: string | undefined, matchStart: boolean, centerpoint: [number, number], features: Feature[]): IntermediateResult[] {
|
||||||
const results: IntermediateResult [] = []
|
const results: IntermediateResult [] = []
|
||||||
|
|
||||||
for (const feature of features) {
|
for (const feature of features) {
|
||||||
|
@ -39,14 +40,19 @@ export default class LocalElementSearch implements GeocodingProvider {
|
||||||
(props["addr:street"] && props["addr:number"]) ?
|
(props["addr:street"] && props["addr:number"]) ?
|
||||||
props["addr:street"] + props["addr:number"] : undefined])
|
props["addr:street"] + props["addr:number"] : undefined])
|
||||||
|
|
||||||
|
let levehnsteinD: number
|
||||||
const levehnsteinD = Math.min(...searchTerms.flatMap(entry => entry.split(/ /)).map(entry => {
|
console.log("Comparing nearby:", candidateId, props.id)
|
||||||
let simplified = Utils.simplifyStringForSearch(entry)
|
if (candidateId === props.id) {
|
||||||
if (matchStart) {
|
levehnsteinD = 0
|
||||||
simplified = simplified.slice(0, query.length)
|
} else {
|
||||||
}
|
levehnsteinD = Math.min(...searchTerms.flatMap(entry => entry.split(/ /)).map(entry => {
|
||||||
return Utils.levenshteinDistance(query, simplified)
|
let simplified = Utils.simplifyStringForSearch(entry)
|
||||||
}))
|
if (matchStart) {
|
||||||
|
simplified = simplified.slice(0, query.length)
|
||||||
|
}
|
||||||
|
return Utils.levenshteinDistance(query, simplified)
|
||||||
|
}))
|
||||||
|
}
|
||||||
const center = GeoOperations.centerpointCoordinates(feature)
|
const center = GeoOperations.centerpointCoordinates(feature)
|
||||||
if (levehnsteinD <= 2) {
|
if (levehnsteinD <= 2) {
|
||||||
|
|
||||||
|
@ -63,7 +69,7 @@ export default class LocalElementSearch implements GeocodingProvider {
|
||||||
physicalDistance: GeoOperations.distanceBetween(centerpoint, center),
|
physicalDistance: GeoOperations.distanceBetween(centerpoint, center),
|
||||||
levehnsteinD,
|
levehnsteinD,
|
||||||
searchTerms,
|
searchTerms,
|
||||||
description: description !== "" ? description : undefined
|
description: description !== "" ? description : undefined,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -77,33 +83,34 @@ export default class LocalElementSearch implements GeocodingProvider {
|
||||||
const center: { lon: number; lat: number } = this._state.mapProperties.location.data
|
const center: { lon: number; lat: number } = this._state.mapProperties.location.data
|
||||||
const centerPoint: [number, number] = [center.lon, center.lat]
|
const centerPoint: [number, number] = [center.lon, center.lat]
|
||||||
const properties = this._state.perLayer
|
const properties = this._state.perLayer
|
||||||
|
const candidateId = OpenStreetMapIdSearch.extractId(query)
|
||||||
query = Utils.simplifyStringForSearch(query)
|
query = Utils.simplifyStringForSearch(query)
|
||||||
|
|
||||||
const partials: Store<IntermediateResult[]>[] = []
|
const partials: Store<IntermediateResult[]>[] = []
|
||||||
|
|
||||||
for (const [_, geoIndexedStore] of properties) {
|
for (const [_, geoIndexedStore] of properties) {
|
||||||
const partialResult = geoIndexedStore.features.map(features => this.getPartialResult(query, matchStart, centerPoint, features))
|
const partialResult = geoIndexedStore.features.map(features => this.getPartialResult(query, candidateId, matchStart, centerPoint, features))
|
||||||
partials.push(partialResult)
|
partials.push(partialResult)
|
||||||
}
|
}
|
||||||
|
|
||||||
const listed: Store<IntermediateResult[]> = Stores.concat(partials)
|
const listed: Store<IntermediateResult[]> = Stores.concat(partials).map(l => l.flatMap(x => x))
|
||||||
return listed.mapD(results => {
|
return listed.mapD(results => {
|
||||||
results.sort((a, b) => (a.physicalDistance + a.levehnsteinD * 25) - (b.physicalDistance + b.levehnsteinD * 25))
|
results.sort((a, b) => (a.physicalDistance + a.levehnsteinD * 25) - (b.physicalDistance + b.levehnsteinD * 25))
|
||||||
if (this._limit || options?.limit) {
|
if (this._limit || options?.limit) {
|
||||||
results = results.slice(0, Math.min(this._limit ?? options?.limit, options?.limit ?? this._limit))
|
results = results.slice(0, Math.min(this._limit ?? options?.limit, options?.limit ?? this._limit))
|
||||||
}
|
}
|
||||||
return results.map(entry => {
|
return results.map(entry => {
|
||||||
const id = entry.feature.properties.id.split("/")
|
const [osm_type, osm_id] = entry.feature.properties.id.split("/")
|
||||||
return <GeoCodeResult>{
|
return <GeoCodeResult>{
|
||||||
lon: entry.center[0],
|
lon: entry.center[0],
|
||||||
lat: entry.center[1],
|
lat: entry.center[1],
|
||||||
osm_type: id[0],
|
osm_type,
|
||||||
osm_id: id[1],
|
osm_id,
|
||||||
display_name: entry.searchTerms[0],
|
display_name: entry.searchTerms[0],
|
||||||
source: "localElementSearch",
|
source: "localElementSearch",
|
||||||
feature: entry.feature,
|
feature: entry.feature,
|
||||||
importance: 1,
|
importance: 1,
|
||||||
description: entry.description
|
description: entry.description,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -4,7 +4,6 @@ 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, { GeoCodeResult } from "./GeocodingProvider"
|
import GeocodingProvider, { GeoCodeResult } from "./GeocodingProvider"
|
||||||
import { Store, UIEventSource } from "../UIEventSource"
|
|
||||||
|
|
||||||
export class NominatimGeocoding implements GeocodingProvider {
|
export class NominatimGeocoding implements GeocodingProvider {
|
||||||
|
|
||||||
|
|
66
src/Logic/Geocoding/OpenStreetMapIdSearch.ts
Normal file
66
src/Logic/Geocoding/OpenStreetMapIdSearch.ts
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
import { Store, UIEventSource } from "../UIEventSource"
|
||||||
|
import GeocodingProvider, { GeoCodeResult, GeocodingOptions } from "./GeocodingProvider"
|
||||||
|
import { OsmId } from "../../Models/OsmFeature"
|
||||||
|
import { SpecialVisualizationState } from "../../UI/SpecialVisualization"
|
||||||
|
|
||||||
|
export default class OpenStreetMapIdSearch implements GeocodingProvider {
|
||||||
|
private static regex = /((https?:\/\/)?(www.)?(osm|openstreetmap).org\/)?(node|way|relation)\/([0-9]+)/
|
||||||
|
|
||||||
|
private readonly _state: SpecialVisualizationState
|
||||||
|
|
||||||
|
constructor(state: SpecialVisualizationState) {
|
||||||
|
this._state = state
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* OpenStreetMapIdSearch.extractId("osm.org/node/42") // => "node/42"
|
||||||
|
* OpenStreetMapIdSearch.extractId("https://openstreetmap.org/node/42#map=19/51.204245/3.212731") // => "node/42"
|
||||||
|
* OpenStreetMapIdSearch.extractId("node/42") // => "node/42"
|
||||||
|
* OpenStreetMapIdSearch.extractId("way/42") // => "way/42"
|
||||||
|
* OpenStreetMapIdSearch.extractId("https://www.openstreetmap.org/node/5212733638") // => "node/5212733638"
|
||||||
|
*/
|
||||||
|
public static extractId(query: string): OsmId | undefined {
|
||||||
|
const match = query.match(OpenStreetMapIdSearch.regex)
|
||||||
|
if (match) {
|
||||||
|
const type = match.at(-2)
|
||||||
|
const id = match.at(-1)
|
||||||
|
return <OsmId>(type + "/" + id)
|
||||||
|
}
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
async search(query: string, options?: GeocodingOptions): Promise<GeoCodeResult[]> {
|
||||||
|
const id = OpenStreetMapIdSearch.extractId(query)
|
||||||
|
if (!id) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
const [osm_type, osm_id] = id.split("/")
|
||||||
|
const obj = await this._state.osmObjectDownloader.DownloadObjectAsync(id)
|
||||||
|
if (obj === "deleted") {
|
||||||
|
return [{
|
||||||
|
display_name: id + " was deleted",
|
||||||
|
category: "coordinate",
|
||||||
|
osm_type: <"node" | "way" | "relation">osm_type,
|
||||||
|
osm_id,
|
||||||
|
lat: 0, lon: 0,
|
||||||
|
source: "osmid"
|
||||||
|
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
const [lat, lon] = obj.centerpoint()
|
||||||
|
return [{
|
||||||
|
lat, lon,
|
||||||
|
display_name: obj.tags.name ?? obj.tags.alt_name ?? obj.tags.local_name ?? obj.tags.ref ?? id,
|
||||||
|
osm_type: <"node" | "way" | "relation">osm_type,
|
||||||
|
osm_id,
|
||||||
|
source: "osmid"
|
||||||
|
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
|
||||||
|
suggest?(query: string, options?: GeocodingOptions): Store<GeoCodeResult[]> {
|
||||||
|
return UIEventSource.FromPromise(this.search(query, options))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -134,7 +134,8 @@ export default class PhotonSearch implements GeocodingProvider, ReverseGeocoding
|
||||||
osm_type: PhotonSearch.types[f.properties.osm_type],
|
osm_type: PhotonSearch.types[f.properties.osm_type],
|
||||||
category: this.getCategory(f),
|
category: this.getCategory(f),
|
||||||
boundingbox,
|
boundingbox,
|
||||||
lon, lat
|
lon, lat,
|
||||||
|
source: this._endpoint
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -41,16 +41,16 @@ export class Stores {
|
||||||
return src
|
return src
|
||||||
}
|
}
|
||||||
|
|
||||||
public static concat<T>(stores: Store<T[]>[]): Store<T[]> {
|
public static concat<T>(stores: Store<T[]>[]): Store<T[][]> {
|
||||||
const newStore = new UIEventSource<T[]>([])
|
const newStore = new UIEventSource<T[][]>([])
|
||||||
function update(){
|
function update(){
|
||||||
if(newStore._callbacks.isDestroyed){
|
if(newStore._callbacks.isDestroyed){
|
||||||
return true // unregister
|
return true // unregister
|
||||||
}
|
}
|
||||||
const results: T[] = []
|
const results: T[][] = []
|
||||||
for (const store of stores) {
|
for (const store of stores) {
|
||||||
if(store.data){
|
if(store.data){
|
||||||
results.push(...store.data)
|
results.push(store.data)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
newStore.setData(results)
|
newStore.setData(results)
|
||||||
|
|
|
@ -75,6 +75,7 @@ import LocalElementSearch from "../Logic/Geocoding/LocalElementSearch"
|
||||||
import { RecentSearch } from "../Logic/Geocoding/RecentSearch"
|
import { RecentSearch } from "../Logic/Geocoding/RecentSearch"
|
||||||
import PhotonSearch from "../Logic/Geocoding/PhotonSearch"
|
import PhotonSearch from "../Logic/Geocoding/PhotonSearch"
|
||||||
import ThemeSearch from "../Logic/Geocoding/ThemeSearch"
|
import ThemeSearch from "../Logic/Geocoding/ThemeSearch"
|
||||||
|
import OpenStreetMapIdSearch from "../Logic/Geocoding/OpenStreetMapIdSearch"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
|
@ -383,9 +384,10 @@ export default class ThemeViewState implements SpecialVisualizationState {
|
||||||
this.toCacheSavers = layout.enableCache ? this.initSaveToLocalStorage() : undefined
|
this.toCacheSavers = layout.enableCache ? this.initSaveToLocalStorage() : undefined
|
||||||
|
|
||||||
this.geosearch = new CombinedSearcher(
|
this.geosearch = new CombinedSearcher(
|
||||||
new LocalElementSearch(this, 5),
|
|
||||||
new PhotonSearch(), // new NominatimGeocoding(),
|
|
||||||
new CoordinateSearch(),
|
new CoordinateSearch(),
|
||||||
|
new LocalElementSearch(this, 5),
|
||||||
|
new OpenStreetMapIdSearch(this),
|
||||||
|
new PhotonSearch(), // new NominatimGeocoding(),
|
||||||
this.featureSwitches.featureSwitchBackToThemeOverview.data ? new ThemeSearch(this) : undefined
|
this.featureSwitches.featureSwitchBackToThemeOverview.data ? new ThemeSearch(this) : undefined
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -960,6 +960,11 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
|
||||||
if (!result["error"]) {
|
if (!result["error"]) {
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
console.log(result)
|
||||||
|
if(result["error"]?.statuscode === 410){
|
||||||
|
// Gone permanently is not recoverable
|
||||||
|
return result
|
||||||
|
}
|
||||||
console.log(
|
console.log(
|
||||||
`Request to ${url} failed, Trying again in a moment. Attempt ${
|
`Request to ${url} failed, Trying again in a moment. Attempt ${
|
||||||
i + 1
|
i + 1
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue