From 4d2b3c9cf7b8e9e171a383414ef0e15784d88c6d Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Fri, 18 Oct 2024 00:10:28 +0200 Subject: [PATCH] Search: add support for OpenLocationCodes and some other coordinate formats, see #2157 --- package-lock.json | 22 ++++++++++ package.json | 2 + src/Logic/Search/CoordinateSearch.ts | 34 ++++++++++++--- src/Logic/Search/OpenLocationCodeSearch.ts | 51 ++++++++++++++++++++++ src/Logic/State/SearchState.ts | 2 + 5 files changed, 106 insertions(+), 5 deletions(-) create mode 100644 src/Logic/Search/OpenLocationCodeSearch.ts diff --git a/package-lock.json b/package-lock.json index 414c706a8..36d91f9b1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -31,6 +31,7 @@ "buffer": "^6.0.3", "chart.js": "^3.8.0", "comunica": "^2.0.0", + "coordinate-parser": "^1.0.7", "country-language": "^0.1.7", "country-to-currency": "^1.0.10", "crypto": "^1.0.1", @@ -69,6 +70,7 @@ "papaparse": "^5.3.1", "pg": "^8.11.3", "pic4carto": "^2.1.15", + "pluscodes": "^2.6.0", "pmtiles": "^3.0.5", "prompt-sync": "^4.2.0", "qrcode-generator": "^1.4.4", @@ -8651,6 +8653,11 @@ "monotone-convex-hull-2d": "^1.0.1" } }, + "node_modules/coordinate-parser": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/coordinate-parser/-/coordinate-parser-1.0.7.tgz", + "integrity": "sha512-pkcjigkAEjU5JsTYnuXLkRgR6T5fF/7GXR4p9vWJesy8fKwsheN8zC5d3sSvdMmWihHB4u48xWZ5mUCcgBIEpw==" + }, "node_modules/core-js": { "version": "2.6.12", "dev": true, @@ -16246,6 +16253,11 @@ "pathe": "^1.0.0" } }, + "node_modules/pluscodes": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/pluscodes/-/pluscodes-2.6.0.tgz", + "integrity": "sha512-+3sW+Qt+znuN2uMFFvebo2m5MsaTjBXOzEYvkfx4RMeOYnNCQv3OWeQujfRAo6nzg7D+5vD2b3tihtwW3b5pfg==" + }, "node_modules/pmtiles": { "version": "3.0.5", "license": "BSD-3-Clause", @@ -27289,6 +27301,11 @@ "monotone-convex-hull-2d": "^1.0.1" } }, + "coordinate-parser": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/coordinate-parser/-/coordinate-parser-1.0.7.tgz", + "integrity": "sha512-pkcjigkAEjU5JsTYnuXLkRgR6T5fF/7GXR4p9vWJesy8fKwsheN8zC5d3sSvdMmWihHB4u48xWZ5mUCcgBIEpw==" + }, "core-js": { "version": "2.6.12", "dev": true @@ -32245,6 +32262,11 @@ "pathe": "^1.0.0" } }, + "pluscodes": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/pluscodes/-/pluscodes-2.6.0.tgz", + "integrity": "sha512-+3sW+Qt+znuN2uMFFvebo2m5MsaTjBXOzEYvkfx4RMeOYnNCQv3OWeQujfRAo6nzg7D+5vD2b3tihtwW3b5pfg==" + }, "pmtiles": { "version": "3.0.5", "requires": { diff --git a/package.json b/package.json index 47b9c2a4f..efc254cdd 100644 --- a/package.json +++ b/package.json @@ -175,6 +175,7 @@ "buffer": "^6.0.3", "chart.js": "^3.8.0", "comunica": "^2.0.0", + "coordinate-parser": "^1.0.7", "country-language": "^0.1.7", "country-to-currency": "^1.0.10", "crypto": "^1.0.1", @@ -213,6 +214,7 @@ "papaparse": "^5.3.1", "pg": "^8.11.3", "pic4carto": "^2.1.15", + "pluscodes": "^2.6.0", "pmtiles": "^3.0.5", "prompt-sync": "^4.2.0", "qrcode-generator": "^1.4.4", diff --git a/src/Logic/Search/CoordinateSearch.ts b/src/Logic/Search/CoordinateSearch.ts index d531da759..d9dd84a2a 100644 --- a/src/Logic/Search/CoordinateSearch.ts +++ b/src/Logic/Search/CoordinateSearch.ts @@ -1,13 +1,15 @@ import GeocodingProvider, { GeocodeResult } from "./GeocodingProvider" import { Utils } from "../../Utils" import { ImmutableStore, Store } from "../UIEventSource" - +import CoordinateParser from "coordinate-parser" /** * A simple search-class which interprets possible locations */ export default class CoordinateSearch implements GeocodingProvider { private static readonly latLonRegexes: ReadonlyArray = [ - /^(-?[0-9]+\.[0-9]+)[ ,;/\\]+(-?[0-9]+\.[0-9]+)/, + /^ *(-?[0-9]+\.[0-9]+)[ ,;/\\]+(-?[0-9]+\.[0-9]+)/, + /^ *(-?[0-9]+,[0-9]+)[ ;/\\]+(-?[0-9]+,[0-9]+)/, + /lat[:=]? *['"]?(-?[0-9]+\.[0-9]+)['"]?[ ,;&]+lon[:=]? *['"]?(-?[0-9]+\.[0-9]+)['"]?/, /lat[:=]? *['"]?(-?[0-9]+\.[0-9]+)['"]?[ ,;&]+lng[:=]? *['"]?(-?[0-9]+\.[0-9]+)['"]?/, @@ -17,6 +19,8 @@ export default class CoordinateSearch implements GeocodingProvider { private static readonly lonLatRegexes: ReadonlyArray = [ /^(-?[0-9]+\.[0-9]+)[ ,;/\\]+(-?[0-9]+\.[0-9]+)/, + /^ *(-?[0-9]+,[0-9]+)[ ;/\\]+(-?[0-9]+,[0-9]+)/, + /lon[:=]? *['"]?(-?[0-9]+\.[0-9]+)['"]?[ ,;&]+lat[:=]? *['"]?(-?[0-9]+\.[0-9]+)['"]?/, /lng[:=]? *['"]?(-?[0-9]+\.[0-9]+)['"]?[ ,;&]+lat[:=]? *['"]?(-?[0-9]+\.[0-9]+)['"]?/, @@ -58,14 +62,31 @@ export default class CoordinateSearch implements GeocodingProvider { * 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.720254, lat: -57.58029", "category": "coordinate","osm_id": "-12.720254/-57.58029", "source": "coordinate:latlon"} + * + * // Should work with commas + * const ls = new CoordinateSearch() + * const results = ls.directSearch('51,047977 3,51184') + * results.length // => 2 + * results[0] // => {lat: 51.047977, lon: 3.51184, "display_name": "lon: 3.51184, lat: 51.047977", "category": "coordinate","osm_id": "3.51184/51.047977", "source": "coordinate:latlon"} */ private directSearch(query: string): GeocodeResult[] { const matches = Utils.NoNull(CoordinateSearch.latLonRegexes.map(r => query.match(r))) - .map(m => CoordinateSearch.asResult(m[2], m[1], "latlon") ) + .map(m => CoordinateSearch.asResult(m[2], m[1], "latlon")) const matchesLonLat = Utils.NoNull(CoordinateSearch.lonLatRegexes.map(r => query.match(r))) .map(m => CoordinateSearch.asResult(m[1], m[2], "lonlat")) - return matches.concat(matchesLonLat) + const init = matches.concat(matchesLonLat) + if (init.length > 0) { + return init + } + + try { + const c = new CoordinateParser(query); + return [CoordinateSearch.asResult(""+c.getLongitude(), ""+c.getLatitude(), "coordinateParser")] + } catch { + return [] + } + } private static round6(n: number): string { @@ -73,6 +94,9 @@ export default class CoordinateSearch implements GeocodingProvider { } private static asResult(lonIn: string, latIn: string, source: string): GeocodeResult { + lonIn = lonIn.replaceAll(",", ".") + latIn = latIn.replaceAll(",", ".") + const lon = Number(lonIn) const lat = Number(latIn) const lonStr = CoordinateSearch.round6(lon) @@ -82,7 +106,7 @@ export default class CoordinateSearch implements GeocodingProvider { lon, display_name: "lon: " + lonStr + ", lat: " + latStr, category: "coordinate", - source: "coordinate:"+source, + source: "coordinate:" + source, osm_id: lonStr + "/" + latStr, } } diff --git a/src/Logic/Search/OpenLocationCodeSearch.ts b/src/Logic/Search/OpenLocationCodeSearch.ts new file mode 100644 index 000000000..84ea9332e --- /dev/null +++ b/src/Logic/Search/OpenLocationCodeSearch.ts @@ -0,0 +1,51 @@ +import { Store, Stores, UIEventSource } from "../UIEventSource" +import GeocodingProvider, { + GeocodeResult, + GeocodingOptions, + ReverseGeocodingProvider, + ReverseGeocodingResult, +} from "./GeocodingProvider" +import { decode as pluscode_decode } from "pluscodes" + +export default class OpenLocationCodeSearch implements GeocodingProvider { + + /** + * A regex describing all plus-codes + */ + public static readonly _isPlusCode = /^([2-9CFGHJMPQRVWX]{2}|00){2,4}\+([2-9CFGHJMPQRVWX]{2,3})?$/ + + /** + * + * OpenLocationCodeSearch.isPlusCode("9FFW84J9+XG") // => true + * OpenLocationCodeSearch.isPlusCode("9FFW84J9+") // => true + * OpenLocationCodeSearch.isPlusCode("9AFW84J9+") // => false + * OpenLocationCodeSearch.isPlusCode("9FFW+") // => true + * OpenLocationCodeSearch.isPlusCode("9FFW0000+") // => true + * OpenLocationCodeSearch.isPlusCode("9FFw0000+") // => true + * OpenLocationCodeSearch.isPlusCode("9FFW000+") // => false + * + */ + public static isPlusCode(str: string) { + return str.toUpperCase().match(this._isPlusCode) !== null + } + + async search(query: string, options?: GeocodingOptions): Promise { + if (!OpenLocationCodeSearch.isPlusCode(query)) { + return undefined + } + const { latitude, longitude } = pluscode_decode(query) + + return [{ + lon: longitude, + lat: latitude, + description: "Open Location Code", + osm_id: query, + display_name: query.toUpperCase(), + }] + } + + suggest?(query: string, options?: GeocodingOptions): Store { + return Stores.FromPromise(this.search(query, options)) + } + +} diff --git a/src/Logic/State/SearchState.ts b/src/Logic/State/SearchState.ts index e6c4a493f..92022d44f 100644 --- a/src/Logic/State/SearchState.ts +++ b/src/Logic/State/SearchState.ts @@ -15,6 +15,7 @@ import LayerSearch from "../Search/LayerSearch" import LayerConfig from "../../Models/ThemeConfig/LayerConfig" import { FeatureSource } from "../FeatureSource/FeatureSource" import { Feature } from "geojson" +import OpenLocationCodeSearch from "../Search/OpenLocationCodeSearch" export default class SearchState { @@ -38,6 +39,7 @@ export default class SearchState { this.locationSearchers = [ new LocalElementSearch(state, 5), new CoordinateSearch(), + new OpenLocationCodeSearch(), new OpenStreetMapIdSearch(state), new PhotonSearch(true, 2), new PhotonSearch(),