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[]> { | ||||
|         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 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"}
 | ||||
|      * 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 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"}
 | ||||
|      * 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 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"}
 | ||||
|      * 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": "coordinate:lonlat"}
 | ||||
|      * | ||||
|      * // test OSM-XML format
 | ||||
|      * const ls = new CoordinateSearch() | ||||
|      * const results = ls.directSearch('  lat="57.5802905" lon="12.7202538"') | ||||
|      * 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
 | ||||
|      * const ls = new CoordinateSearch() | ||||
|      * const results = ls.directSearch('  lat="-57.5802905" lon="-12.7202538"') | ||||
|      * 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[] { | ||||
| 
 | ||||
|  | @ -58,7 +58,7 @@ export default class CoordinateSearch implements GeocodingProvider { | |||
|             lat: Number(m[1]), | ||||
|             lon: Number(m[2]), | ||||
|             display_name: "lon: " + m[2] + ", lat: " + m[1], | ||||
|             source: "coordinateSearch", | ||||
|             source: "coordinate:latlon", | ||||
|             category: "coordinate" | ||||
|         }) | ||||
| 
 | ||||
|  | @ -68,8 +68,8 @@ export default class CoordinateSearch implements GeocodingProvider { | |||
|                 lat: Number(m[2]), | ||||
|                 lon: Number(m[1]), | ||||
|                 display_name: "lon: " + m[1] + ", lat: " + m[2], | ||||
|                 source: "coordinateSearch", | ||||
|                 category: "coordinate" | ||||
|                 category: "coordinate", | ||||
|                 source: "coordinate:lonlat" | ||||
|             }) | ||||
|         return matches.concat(matchesLonLat) | ||||
|     } | ||||
|  |  | |||
|  | @ -27,7 +27,8 @@ export default class GeocodingFeatureSource implements FeatureSource { | |||
|                         display_name: gc.display_name, | ||||
|                         osm_id: gc.osm_type + "/" + gc.osm_id, | ||||
|                         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: { | ||||
|                         type: "Point", | ||||
|  |  | |||
|  | @ -27,7 +27,8 @@ export type GeoCodeResult = { | |||
|     osm_type?: "node" | "way" | "relation" | ||||
|     osm_id?: string, | ||||
|     category?: GeocodingCategory, | ||||
|     payload?: object | ||||
|     payload?: object, | ||||
|     source?: string | ||||
| } | ||||
| 
 | ||||
| export interface GeocodingOptions { | ||||
|  |  | |||
|  | @ -4,6 +4,7 @@ import { Utils } from "../../Utils" | |||
| import { Feature } from "geojson" | ||||
| import { GeoOperations } from "../GeoOperations" | ||||
| import { ImmutableStore, Store, Stores } from "../UIEventSource" | ||||
| import OpenStreetMapIdSearch from "./OpenStreetMapIdSearch" | ||||
| 
 | ||||
| type IntermediateResult = { | ||||
|     feature: Feature, | ||||
|  | @ -30,7 +31,7 @@ export default class LocalElementSearch implements GeocodingProvider { | |||
|         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 [] = [] | ||||
| 
 | ||||
|         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"] : 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) | ||||
|             })) | ||||
|             let levehnsteinD: number | ||||
|             console.log("Comparing nearby:", candidateId, props.id) | ||||
|             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) | ||||
|                 })) | ||||
|             } | ||||
|             const center = GeoOperations.centerpointCoordinates(feature) | ||||
|             if (levehnsteinD <= 2) { | ||||
| 
 | ||||
|  | @ -63,7 +69,7 @@ export default class LocalElementSearch implements GeocodingProvider { | |||
|                     physicalDistance: GeoOperations.distanceBetween(centerpoint, center), | ||||
|                     levehnsteinD, | ||||
|                     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 centerPoint: [number, number] = [center.lon, center.lat] | ||||
|         const properties = this._state.perLayer | ||||
|         const candidateId = OpenStreetMapIdSearch.extractId(query) | ||||
|         query = Utils.simplifyStringForSearch(query) | ||||
| 
 | ||||
|         const partials: Store<IntermediateResult[]>[] = [] | ||||
| 
 | ||||
|         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) | ||||
|         } | ||||
| 
 | ||||
|         const listed: Store<IntermediateResult[]> = Stores.concat(partials) | ||||
|         const listed: Store<IntermediateResult[]> = Stores.concat(partials).map(l => l.flatMap(x => x)) | ||||
|         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("/") | ||||
|                 const [osm_type, osm_id] = entry.feature.properties.id.split("/") | ||||
|                 return <GeoCodeResult>{ | ||||
|                     lon: entry.center[0], | ||||
|                     lat: entry.center[1], | ||||
|                     osm_type: id[0], | ||||
|                     osm_id: id[1], | ||||
|                     osm_type, | ||||
|                     osm_id, | ||||
|                     display_name: entry.searchTerms[0], | ||||
|                     source: "localElementSearch", | ||||
|                     feature: entry.feature, | ||||
|                     importance: 1, | ||||
|                     description: entry.description | ||||
|                     description: entry.description, | ||||
|                 } | ||||
|             }) | ||||
|         }) | ||||
|  |  | |||
|  | @ -4,7 +4,6 @@ import Constants from "../../Models/Constants" | |||
| import { FeatureCollection } from "geojson" | ||||
| import Locale from "../../UI/i18n/Locale" | ||||
| import GeocodingProvider, { GeoCodeResult } from "./GeocodingProvider" | ||||
| import { Store, UIEventSource } from "../UIEventSource" | ||||
| 
 | ||||
| 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], | ||||
|                 category: this.getCategory(f), | ||||
|                 boundingbox, | ||||
|                 lon, lat | ||||
|                 lon, lat, | ||||
|                 source: this._endpoint | ||||
|             } | ||||
|         }) | ||||
|     } | ||||
|  |  | |||
|  | @ -41,16 +41,16 @@ export class Stores { | |||
|         return src | ||||
|     } | ||||
| 
 | ||||
|     public static concat<T>(stores: Store<T[]>[]): Store<T[]> { | ||||
|         const newStore = new UIEventSource<T[]>([]) | ||||
|     public static concat<T>(stores: Store<T[]>[]): Store<T[][]> { | ||||
|         const newStore = new UIEventSource<T[][]>([]) | ||||
|         function update(){ | ||||
|             if(newStore._callbacks.isDestroyed){ | ||||
|                 return true // unregister
 | ||||
|             } | ||||
|             const results: T[] = [] | ||||
|             const results: T[][] = [] | ||||
|             for (const store of stores) { | ||||
|                 if(store.data){ | ||||
|                     results.push(...store.data) | ||||
|                     results.push(store.data) | ||||
|                 } | ||||
|             } | ||||
|             newStore.setData(results) | ||||
|  |  | |||
|  | @ -75,6 +75,7 @@ import LocalElementSearch from "../Logic/Geocoding/LocalElementSearch" | |||
| import { RecentSearch } from "../Logic/Geocoding/RecentSearch" | ||||
| import PhotonSearch from "../Logic/Geocoding/PhotonSearch" | ||||
| 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.geosearch = new CombinedSearcher( | ||||
|             new LocalElementSearch(this, 5), | ||||
|             new PhotonSearch(), // new NominatimGeocoding(),
 | ||||
|             new CoordinateSearch(), | ||||
|             new LocalElementSearch(this, 5), | ||||
|             new OpenStreetMapIdSearch(this), | ||||
|             new PhotonSearch(), // new NominatimGeocoding(),
 | ||||
|             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"]) { | ||||
|                 return result | ||||
|             } | ||||
|             console.log(result) | ||||
|             if(result["error"]?.statuscode === 410){ | ||||
|                 // Gone permanently is not recoverable
 | ||||
|                 return result | ||||
|             } | ||||
|             console.log( | ||||
|                 `Request to ${url} failed, Trying again in a moment. Attempt ${ | ||||
|                     i + 1 | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue