From 3ab1a0a3f2c72d8374e299c0a98ebabcb515752d Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Sun, 25 Aug 2024 02:40:56 +0200 Subject: [PATCH] Search: add support for osm.org urls such as osm.org/node/42 --- src/Logic/Geocoding/CombinedSearcher.ts | 1 + src/Logic/Geocoding/CoordinateSearch.ts | 18 ++--- src/Logic/Geocoding/GeocodingFeatureSource.ts | 3 +- src/Logic/Geocoding/GeocodingProvider.ts | 3 +- src/Logic/Geocoding/LocalElementSearch.ts | 39 ++++++----- src/Logic/Geocoding/NominatimGeocoding.ts | 1 - src/Logic/Geocoding/OpenStreetMapIdSearch.ts | 66 +++++++++++++++++++ src/Logic/Geocoding/PhotonSearch.ts | 3 +- src/Logic/UIEventSource.ts | 8 +-- src/Models/ThemeViewState.ts | 6 +- src/Utils.ts | 5 ++ 11 files changed, 118 insertions(+), 35 deletions(-) create mode 100644 src/Logic/Geocoding/OpenStreetMapIdSearch.ts diff --git a/src/Logic/Geocoding/CombinedSearcher.ts b/src/Logic/Geocoding/CombinedSearcher.ts index bd2539f14..3ee38e016 100644 --- a/src/Logic/Geocoding/CombinedSearcher.ts +++ b/src/Logic/Geocoding/CombinedSearcher.ts @@ -40,6 +40,7 @@ export default class CombinedSearcher implements GeocodingProvider { suggest(query: string, options?: GeocodingOptions): Store { return Stores.concat(this._providersWithSuggest.map(pr => pr.suggest(query, options))) + .map(gcrss => this.merge(gcrss)) } } diff --git a/src/Logic/Geocoding/CoordinateSearch.ts b/src/Logic/Geocoding/CoordinateSearch.ts index 5036d67bb..7aa34ac95 100644 --- a/src/Logic/Geocoding/CoordinateSearch.ts +++ b/src/Logic/Geocoding/CoordinateSearch.ts @@ -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) } diff --git a/src/Logic/Geocoding/GeocodingFeatureSource.ts b/src/Logic/Geocoding/GeocodingFeatureSource.ts index 943c8fbcd..2278edff7 100644 --- a/src/Logic/Geocoding/GeocodingFeatureSource.ts +++ b/src/Logic/Geocoding/GeocodingFeatureSource.ts @@ -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", diff --git a/src/Logic/Geocoding/GeocodingProvider.ts b/src/Logic/Geocoding/GeocodingProvider.ts index bcf50d200..2fac696d6 100644 --- a/src/Logic/Geocoding/GeocodingProvider.ts +++ b/src/Logic/Geocoding/GeocodingProvider.ts @@ -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 { diff --git a/src/Logic/Geocoding/LocalElementSearch.ts b/src/Logic/Geocoding/LocalElementSearch.ts index 4aaa116cf..16661dbc2 100644 --- a/src/Logic/Geocoding/LocalElementSearch.ts +++ b/src/Logic/Geocoding/LocalElementSearch.ts @@ -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[] = [] 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 = Stores.concat(partials) + const listed: Store = 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 { 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, } }) }) diff --git a/src/Logic/Geocoding/NominatimGeocoding.ts b/src/Logic/Geocoding/NominatimGeocoding.ts index 691ba8b1d..3b5c4fd3f 100644 --- a/src/Logic/Geocoding/NominatimGeocoding.ts +++ b/src/Logic/Geocoding/NominatimGeocoding.ts @@ -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 { diff --git a/src/Logic/Geocoding/OpenStreetMapIdSearch.ts b/src/Logic/Geocoding/OpenStreetMapIdSearch.ts new file mode 100644 index 000000000..ac1196ae7 --- /dev/null +++ b/src/Logic/Geocoding/OpenStreetMapIdSearch.ts @@ -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 (type + "/" + id) + } + return undefined + } + + async search(query: string, options?: GeocodingOptions): Promise { + 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 { + return UIEventSource.FromPromise(this.search(query, options)) + } + +} diff --git a/src/Logic/Geocoding/PhotonSearch.ts b/src/Logic/Geocoding/PhotonSearch.ts index 69c287697..c14e4a4ee 100644 --- a/src/Logic/Geocoding/PhotonSearch.ts +++ b/src/Logic/Geocoding/PhotonSearch.ts @@ -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 } }) } diff --git a/src/Logic/UIEventSource.ts b/src/Logic/UIEventSource.ts index f79201f54..ff347f02e 100644 --- a/src/Logic/UIEventSource.ts +++ b/src/Logic/UIEventSource.ts @@ -41,16 +41,16 @@ export class Stores { return src } - public static concat(stores: Store[]): Store { - const newStore = new UIEventSource([]) + public static concat(stores: Store[]): Store { + const newStore = new UIEventSource([]) 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) diff --git a/src/Models/ThemeViewState.ts b/src/Models/ThemeViewState.ts index 778a7ad94..6e2f71d19 100644 --- a/src/Models/ThemeViewState.ts +++ b/src/Models/ThemeViewState.ts @@ -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 ) diff --git a/src/Utils.ts b/src/Utils.ts index cb0ac4cd5..819b72b15 100644 --- a/src/Utils.ts +++ b/src/Utils.ts @@ -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