forked from MapComplete/MapComplete
		
	Search: add support for OpenLocationCodes and some other coordinate formats, see #2157
This commit is contained in:
		
							parent
							
								
									181219928c
								
							
						
					
					
						commit
						4d2b3c9cf7
					
				
					 5 changed files with 106 additions and 5 deletions
				
			
		
							
								
								
									
										22
									
								
								package-lock.json
									
										
									
										generated
									
									
									
								
							
							
						
						
									
										22
									
								
								package-lock.json
									
										
									
										generated
									
									
									
								
							|  | @ -31,6 +31,7 @@ | ||||||
|         "buffer": "^6.0.3", |         "buffer": "^6.0.3", | ||||||
|         "chart.js": "^3.8.0", |         "chart.js": "^3.8.0", | ||||||
|         "comunica": "^2.0.0", |         "comunica": "^2.0.0", | ||||||
|  |         "coordinate-parser": "^1.0.7", | ||||||
|         "country-language": "^0.1.7", |         "country-language": "^0.1.7", | ||||||
|         "country-to-currency": "^1.0.10", |         "country-to-currency": "^1.0.10", | ||||||
|         "crypto": "^1.0.1", |         "crypto": "^1.0.1", | ||||||
|  | @ -69,6 +70,7 @@ | ||||||
|         "papaparse": "^5.3.1", |         "papaparse": "^5.3.1", | ||||||
|         "pg": "^8.11.3", |         "pg": "^8.11.3", | ||||||
|         "pic4carto": "^2.1.15", |         "pic4carto": "^2.1.15", | ||||||
|  |         "pluscodes": "^2.6.0", | ||||||
|         "pmtiles": "^3.0.5", |         "pmtiles": "^3.0.5", | ||||||
|         "prompt-sync": "^4.2.0", |         "prompt-sync": "^4.2.0", | ||||||
|         "qrcode-generator": "^1.4.4", |         "qrcode-generator": "^1.4.4", | ||||||
|  | @ -8651,6 +8653,11 @@ | ||||||
|         "monotone-convex-hull-2d": "^1.0.1" |         "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": { |     "node_modules/core-js": { | ||||||
|       "version": "2.6.12", |       "version": "2.6.12", | ||||||
|       "dev": true, |       "dev": true, | ||||||
|  | @ -16246,6 +16253,11 @@ | ||||||
|         "pathe": "^1.0.0" |         "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": { |     "node_modules/pmtiles": { | ||||||
|       "version": "3.0.5", |       "version": "3.0.5", | ||||||
|       "license": "BSD-3-Clause", |       "license": "BSD-3-Clause", | ||||||
|  | @ -27289,6 +27301,11 @@ | ||||||
|         "monotone-convex-hull-2d": "^1.0.1" |         "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": { |     "core-js": { | ||||||
|       "version": "2.6.12", |       "version": "2.6.12", | ||||||
|       "dev": true |       "dev": true | ||||||
|  | @ -32245,6 +32262,11 @@ | ||||||
|         "pathe": "^1.0.0" |         "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": { |     "pmtiles": { | ||||||
|       "version": "3.0.5", |       "version": "3.0.5", | ||||||
|       "requires": { |       "requires": { | ||||||
|  |  | ||||||
|  | @ -175,6 +175,7 @@ | ||||||
|     "buffer": "^6.0.3", |     "buffer": "^6.0.3", | ||||||
|     "chart.js": "^3.8.0", |     "chart.js": "^3.8.0", | ||||||
|     "comunica": "^2.0.0", |     "comunica": "^2.0.0", | ||||||
|  |     "coordinate-parser": "^1.0.7", | ||||||
|     "country-language": "^0.1.7", |     "country-language": "^0.1.7", | ||||||
|     "country-to-currency": "^1.0.10", |     "country-to-currency": "^1.0.10", | ||||||
|     "crypto": "^1.0.1", |     "crypto": "^1.0.1", | ||||||
|  | @ -213,6 +214,7 @@ | ||||||
|     "papaparse": "^5.3.1", |     "papaparse": "^5.3.1", | ||||||
|     "pg": "^8.11.3", |     "pg": "^8.11.3", | ||||||
|     "pic4carto": "^2.1.15", |     "pic4carto": "^2.1.15", | ||||||
|  |     "pluscodes": "^2.6.0", | ||||||
|     "pmtiles": "^3.0.5", |     "pmtiles": "^3.0.5", | ||||||
|     "prompt-sync": "^4.2.0", |     "prompt-sync": "^4.2.0", | ||||||
|     "qrcode-generator": "^1.4.4", |     "qrcode-generator": "^1.4.4", | ||||||
|  |  | ||||||
|  | @ -1,13 +1,15 @@ | ||||||
| import GeocodingProvider, { GeocodeResult } from "./GeocodingProvider" | import GeocodingProvider, { GeocodeResult } from "./GeocodingProvider" | ||||||
| import { Utils } from "../../Utils" | import { Utils } from "../../Utils" | ||||||
| import { ImmutableStore, Store } from "../UIEventSource" | import { ImmutableStore, Store } from "../UIEventSource" | ||||||
| 
 | import CoordinateParser from "coordinate-parser" | ||||||
| /** | /** | ||||||
|  * A simple search-class which interprets possible locations |  * A simple search-class which interprets possible locations | ||||||
|  */ |  */ | ||||||
| export default class CoordinateSearch implements GeocodingProvider { | export default class CoordinateSearch implements GeocodingProvider { | ||||||
|     private static readonly latLonRegexes: ReadonlyArray<RegExp> = [ |     private static readonly latLonRegexes: ReadonlyArray<RegExp> = [ | ||||||
|         /^(-?[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]+)['"]?[ ,;&]+lon[:=]? *['"]?(-?[0-9]+\.[0-9]+)['"]?/, | ||||||
|         /lat[:=]? *['"]?(-?[0-9]+\.[0-9]+)['"]?[ ,;&]+lng[:=]? *['"]?(-?[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<RegExp> = [ |     private static readonly lonLatRegexes: ReadonlyArray<RegExp> = [ | ||||||
|         /^(-?[0-9]+\.[0-9]+)[ ,;/\\]+(-?[0-9]+\.[0-9]+)/, |         /^(-?[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]+)['"]?/, |         /lon[:=]? *['"]?(-?[0-9]+\.[0-9]+)['"]?[ ,;&]+lat[:=]? *['"]?(-?[0-9]+\.[0-9]+)['"]?/, | ||||||
|         /lng[:=]? *['"]?(-?[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"') |      * 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.720254, lat: -57.58029",  "category": "coordinate","osm_id": "-12.720254/-57.58029", "source": "coordinate:latlon"}
 |      * 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[] { |     private directSearch(query: string): GeocodeResult[] { | ||||||
|         const matches = Utils.NoNull(CoordinateSearch.latLonRegexes.map(r => query.match(r))) |         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))) |         const matchesLonLat = Utils.NoNull(CoordinateSearch.lonLatRegexes.map(r => query.match(r))) | ||||||
|             .map(m => CoordinateSearch.asResult(m[1], m[2], "lonlat")) |             .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 { |     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 { |     private static asResult(lonIn: string, latIn: string, source: string): GeocodeResult { | ||||||
|  |         lonIn = lonIn.replaceAll(",", ".") | ||||||
|  |         latIn = latIn.replaceAll(",", ".") | ||||||
|  | 
 | ||||||
|         const lon = Number(lonIn) |         const lon = Number(lonIn) | ||||||
|         const lat = Number(latIn) |         const lat = Number(latIn) | ||||||
|         const lonStr = CoordinateSearch.round6(lon) |         const lonStr = CoordinateSearch.round6(lon) | ||||||
|  | @ -82,7 +106,7 @@ export default class CoordinateSearch implements GeocodingProvider { | ||||||
|             lon, |             lon, | ||||||
|             display_name: "lon: " + lonStr + ", lat: " + latStr, |             display_name: "lon: " + lonStr + ", lat: " + latStr, | ||||||
|             category: "coordinate", |             category: "coordinate", | ||||||
|             source: "coordinate:"+source, |             source: "coordinate:" + source, | ||||||
|             osm_id: lonStr + "/" + latStr, |             osm_id: lonStr + "/" + latStr, | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
							
								
								
									
										51
									
								
								src/Logic/Search/OpenLocationCodeSearch.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								src/Logic/Search/OpenLocationCodeSearch.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -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<GeocodeResult[]> { | ||||||
|  |         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<GeocodeResult[]> { | ||||||
|  |         return Stores.FromPromise(this.search(query, options)) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | @ -15,6 +15,7 @@ import LayerSearch from "../Search/LayerSearch" | ||||||
| import LayerConfig from "../../Models/ThemeConfig/LayerConfig" | import LayerConfig from "../../Models/ThemeConfig/LayerConfig" | ||||||
| import { FeatureSource } from "../FeatureSource/FeatureSource" | import { FeatureSource } from "../FeatureSource/FeatureSource" | ||||||
| import { Feature } from "geojson" | import { Feature } from "geojson" | ||||||
|  | import OpenLocationCodeSearch from "../Search/OpenLocationCodeSearch" | ||||||
| 
 | 
 | ||||||
| export default class SearchState { | export default class SearchState { | ||||||
| 
 | 
 | ||||||
|  | @ -38,6 +39,7 @@ export default class SearchState { | ||||||
|         this.locationSearchers = [ |         this.locationSearchers = [ | ||||||
|             new LocalElementSearch(state, 5), |             new LocalElementSearch(state, 5), | ||||||
|             new CoordinateSearch(), |             new CoordinateSearch(), | ||||||
|  |             new OpenLocationCodeSearch(), | ||||||
|             new OpenStreetMapIdSearch(state), |             new OpenStreetMapIdSearch(state), | ||||||
|             new PhotonSearch(true, 2), |             new PhotonSearch(true, 2), | ||||||
|             new PhotonSearch(), |             new PhotonSearch(), | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue