|
| 1 | +import { Injectable } from '@angular/core'; |
| 2 | +import { HttpClient, HttpParams } from '@angular/common/http'; |
| 3 | +import { Observable } from 'rxjs'; |
| 4 | +import { environment } from '../../../environments/environment'; |
| 5 | +import { map, take } from 'rxjs/operators'; |
| 6 | +import { hasValue } from '../../shared/empty.util'; |
| 7 | + |
| 8 | +export interface LocationCoordinates { |
| 9 | + latitude: number, |
| 10 | + longitude: number, |
| 11 | +} |
| 12 | + |
| 13 | +export interface LocationPlace { |
| 14 | + coordinates?: LocationCoordinates, |
| 15 | + displayName?: string, |
| 16 | +} |
| 17 | + |
| 18 | +export enum LocationErrorCodes { |
| 19 | + // define a `location.error.*` i18n label for each error code |
| 20 | + INVALID_COORDINATES = 'invalid-coordinates', |
| 21 | + LOCATION_NOT_FOUND = 'location-not-found', |
| 22 | +} |
| 23 | + |
| 24 | +const IS_COORDINATE_PAIR_REGEXP = /^\d+\.?\d*,\d+\.?\d*$/; |
| 25 | + |
| 26 | +const NOMINATIM_RESPONSE_FORMAT = 'jsonv2'; |
| 27 | + |
| 28 | +@Injectable({ |
| 29 | + providedIn: 'root' |
| 30 | +}) |
| 31 | +export class LocationService { |
| 32 | + |
| 33 | + searchEndpoint = environment.location.nominatimApi.searchEndpoint; |
| 34 | + reverseSearchEndpoint = environment.location.nominatimApi.reverseSearchEndpoint; |
| 35 | + |
| 36 | + constructor( |
| 37 | + protected http: HttpClient, |
| 38 | + ) { } |
| 39 | + |
| 40 | + /** |
| 41 | + * Search a place (address or POI) with Nominatim, and return the coordinates and the display name of the most relevant search result |
| 42 | + * @param address the place to be searched |
| 43 | + * @returns {LocationPlace} the information related to the searched place |
| 44 | + */ |
| 45 | + public searchPlace(address: string): Observable<LocationPlace> { |
| 46 | + let params = new HttpParams().append('q', address).append('format', NOMINATIM_RESPONSE_FORMAT); |
| 47 | + |
| 48 | + return this.http.get<Record<string,any>[]>(this.searchEndpoint, { params: params }).pipe( |
| 49 | + take(1), |
| 50 | + map((searchResults) => { |
| 51 | + if (searchResults.length > 1) { |
| 52 | + console.warn(`Multiple locations found for address "${address}", showing top matches`, searchResults.slice(0,5)); |
| 53 | + } |
| 54 | + if (searchResults.length > 0) { |
| 55 | + const firstMatch = searchResults[0]; |
| 56 | + const coordinates: LocationCoordinates = { |
| 57 | + latitude: parseFloat(firstMatch.lat), |
| 58 | + longitude: parseFloat(firstMatch.lon), |
| 59 | + }; |
| 60 | + const info: LocationPlace = { |
| 61 | + coordinates: coordinates, |
| 62 | + displayName: firstMatch.display_name, |
| 63 | + }; |
| 64 | + return info; |
| 65 | + } else { |
| 66 | + console.warn(`Location service: location "${address}" not found`); |
| 67 | + throw new Error(LocationErrorCodes.LOCATION_NOT_FOUND); |
| 68 | + } |
| 69 | + }), |
| 70 | + ); |
| 71 | + } |
| 72 | + |
| 73 | + /** |
| 74 | + * Search coordinates with Nominatim and return the display name |
| 75 | + * @param coordinates |
| 76 | + * @returns {Observable<string>} the display name for the coordinates |
| 77 | + */ |
| 78 | + public searchCoordinates(coordinates: LocationCoordinates): Observable<string> { |
| 79 | + let params = new HttpParams() |
| 80 | + .append('lat', coordinates.latitude) |
| 81 | + .append('lon', coordinates.longitude) |
| 82 | + .append('format', NOMINATIM_RESPONSE_FORMAT); |
| 83 | + |
| 84 | + return this.http.get<Record<string,any>>(this.reverseSearchEndpoint, { params: params }).pipe( |
| 85 | + take(1), |
| 86 | + map((searchResults) => { |
| 87 | + if (hasValue(searchResults.error)) { |
| 88 | + throw new Error(searchResults.error); |
| 89 | + } else { |
| 90 | + return searchResults.display_name; |
| 91 | + } |
| 92 | + }), |
| 93 | + ); |
| 94 | + } |
| 95 | + |
| 96 | + /** |
| 97 | + * Check if the provided pair of coordinates is valid |
| 98 | + * @param latitude the latitude as float number |
| 99 | + * @param longitude the longitude as float number |
| 100 | + * @returns {boolean} whether the coordinates are valid |
| 101 | + */ |
| 102 | + public isValidCoordinatePair(latitude: number, longitude: number): boolean { |
| 103 | + return !(isNaN(latitude) || isNaN(longitude) || latitude > 90 || latitude < -90 || longitude > 180 || longitude < -180); |
| 104 | + } |
| 105 | + |
| 106 | + /** |
| 107 | + * Check if a string is in the format `latitude,longitude` |
| 108 | + * @param coordinateString the string to be checked |
| 109 | + * @returns {boolean} whether the string has a valid format |
| 110 | + */ |
| 111 | + public isCoordinateString(coordinateString: string): boolean { |
| 112 | + return IS_COORDINATE_PAIR_REGEXP.test(coordinateString) && coordinateString.split(',').length === 2; |
| 113 | + } |
| 114 | + |
| 115 | + /** |
| 116 | + * Check if a string contains valid coordinateString in the format `latitude,longitude` |
| 117 | + * @param coordinateString the string to be checked |
| 118 | + * @returns {boolean} whether the string is valid and it contains valid coordinateString |
| 119 | + */ |
| 120 | + public isValidCoordinateString(coordinateString: string): boolean { |
| 121 | + if (this.isCoordinateString(coordinateString)) { |
| 122 | + const coordinateArray = coordinateString.split(','); |
| 123 | + const latitude = parseFloat(coordinateArray[0]); |
| 124 | + const longitude = parseFloat(coordinateArray[1]); |
| 125 | + return !isNaN(latitude) && !isNaN(longitude) && this.isValidCoordinatePair(latitude, longitude); |
| 126 | + } else { |
| 127 | + return false; |
| 128 | + } |
| 129 | + } |
| 130 | + |
| 131 | + /** |
| 132 | + * Parse a string containing location coordinates, and return an object containing latitude and longitude as numbers |
| 133 | + * @param coordinates a string in the format `latitude,longitude` |
| 134 | + * @returns {LocationCoordinates} the parsed coordinates |
| 135 | + */ |
| 136 | + public parseCoordinates(coordinates: string): LocationCoordinates { |
| 137 | + const coordinateArr = coordinates.split(','); |
| 138 | + if (this.isValidCoordinateString(coordinates)) { |
| 139 | + const latitude = parseFloat(coordinateArr[0]); |
| 140 | + const longitude = parseFloat(coordinateArr[1]); |
| 141 | + return { latitude, longitude }; |
| 142 | + } else { |
| 143 | + console.warn(`Location service: invalid coordinates "${coordinates}"`); |
| 144 | + throw new Error(LocationErrorCodes.INVALID_COORDINATES); |
| 145 | + } |
| 146 | + } |
| 147 | +} |
0 commit comments