Skip to content

Commit 8f57f71

Browse files
author
Andrea Barbasso
committed
[UXP-155] porting of UXP-120 for SG coordinates
1 parent 3806edd commit 8f57f71

3 files changed

Lines changed: 129 additions & 60 deletions

File tree

src/app/core/services/location.service.spec.ts

Lines changed: 22 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -21,19 +21,19 @@ describe('LocationService', () => {
2121

2222
describe('Test utility methods', () => {
2323
it('isCoordinateString() should validate coordinate strings correctly', () => {
24-
expect(service.isCoordinateString(null)).toBeFalse(); // invalid pattern
25-
expect(service.isCoordinateString(undefined)).toBeFalse(); // invalid pattern
26-
expect(service.isCoordinateString('qwerty')).toBeFalse(); // invalid pattern
27-
expect(service.isCoordinateString('45')).toBeFalse(); // invalid pattern, wrong array size
28-
expect(service.isCoordinateString('45,')).toBeFalse(); // invalid pattern, wrong array size
29-
expect(service.isCoordinateString(',45')).toBeFalse(); // invalid pattern, wrong array size
30-
expect(service.isCoordinateString('45,45')).toBeTrue();
31-
expect(service.isCoordinateString('45,45,')).toBeFalse(); // invalid pattern, wrong array size
32-
expect(service.isCoordinateString('45,45,45')).toBeFalse(); // invalid pattern, wrong array size
33-
expect(service.isCoordinateString('.0,.0')).toBeFalse(); // valid numbers, but invalid pattern
34-
expect(service.isCoordinateString('45.000,45.000')).toBeTrue();
35-
expect(service.isCoordinateString('45.000, 45.000')).toBeFalse(); // it contains a space
36-
expect(service.isCoordinateString('200,200')).toBeTrue(); // invalid numbers, but valid pattern
24+
expect(service.isDecimalCoordinateString(null)).toBeFalse(); // invalid pattern
25+
expect(service.isDecimalCoordinateString(undefined)).toBeFalse(); // invalid pattern
26+
expect(service.isDecimalCoordinateString('qwerty')).toBeFalse(); // invalid pattern
27+
expect(service.isDecimalCoordinateString('45')).toBeFalse(); // invalid pattern, wrong array size
28+
expect(service.isDecimalCoordinateString('45,')).toBeFalse(); // invalid pattern, wrong array size
29+
expect(service.isDecimalCoordinateString(',45')).toBeFalse(); // invalid pattern, wrong array size
30+
expect(service.isDecimalCoordinateString('45,45')).toBeTrue();
31+
expect(service.isDecimalCoordinateString('45,45,')).toBeFalse(); // invalid pattern, wrong array size
32+
expect(service.isDecimalCoordinateString('45,45,45')).toBeFalse(); // invalid pattern, wrong array size
33+
expect(service.isDecimalCoordinateString('.0,.0')).toBeFalse(); // valid numbers, but invalid pattern
34+
expect(service.isDecimalCoordinateString('45.000,45.000')).toBeTrue();
35+
expect(service.isDecimalCoordinateString('45.000, 45.000')).toBeFalse(); // it contains a space
36+
expect(service.isDecimalCoordinateString('200,200')).toBeTrue(); // invalid numbers, but valid pattern
3737
});
3838

3939
it('isValidCoordinateString() should validate coordinate strings and check for their values correctly', () => {
@@ -42,15 +42,15 @@ describe('LocationService', () => {
4242
});
4343

4444
it('should validate coordinate pairs correctly', () => {
45-
expect(service.isValidCoordinatePair(0, 0)).toBeTrue();
46-
expect(service.isValidCoordinatePair(45, 45)).toBeTrue();
47-
expect(service.isValidCoordinatePair(-45, -45)).toBeTrue();
48-
expect(service.isValidCoordinatePair(200, 0)).toBeFalse();
49-
expect(service.isValidCoordinatePair(0, 200)).toBeFalse();
50-
expect(service.isValidCoordinatePair(-200, 0)).toBeFalse();
51-
expect(service.isValidCoordinatePair(0, -200)).toBeFalse();
52-
expect(service.isValidCoordinatePair(NaN, 0)).toBeFalse();
53-
expect(service.isValidCoordinatePair(0, NaN)).toBeFalse();
45+
expect(service.isValidDecimalCoordinatePair(0, 0)).toBeTrue();
46+
expect(service.isValidDecimalCoordinatePair(45, 45)).toBeTrue();
47+
expect(service.isValidDecimalCoordinatePair(-45, -45)).toBeTrue();
48+
expect(service.isValidDecimalCoordinatePair(200, 0)).toBeFalse();
49+
expect(service.isValidDecimalCoordinatePair(0, 200)).toBeFalse();
50+
expect(service.isValidDecimalCoordinatePair(-200, 0)).toBeFalse();
51+
expect(service.isValidDecimalCoordinatePair(0, -200)).toBeFalse();
52+
expect(service.isValidDecimalCoordinatePair(NaN, 0)).toBeFalse();
53+
expect(service.isValidDecimalCoordinatePair(0, NaN)).toBeFalse();
5454
});
5555
});
5656
});

src/app/core/services/location.service.ts

Lines changed: 72 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,16 @@ import { environment } from '../../../environments/environment';
55
import { catchError, map, take } from 'rxjs/operators';
66
import { hasValue } from '../../shared/empty.util';
77

8-
export interface LocationCoordinates {
9-
latitude: number,
10-
longitude: number,
8+
/**
9+
* Geographic coordinates in Decimal Degree notation
10+
*/
11+
export interface LocationDDCoordinates {
12+
latitude: number | string,
13+
longitude: number | string,
1114
}
1215

1316
export interface LocationPlace {
14-
coordinates?: LocationCoordinates,
17+
coordinates?: LocationDDCoordinates,
1518
displayName?: string,
1619
}
1720

@@ -22,7 +25,8 @@ export enum LocationErrorCodes {
2225
API_ERROR = 'api-error',
2326
}
2427

25-
const IS_COORDINATE_PAIR_REGEXP = /^\d+\.?\d*,\d+\.?\d*$/;
28+
const IS_DD_COORDINATE_PAIR_REGEXP = /^\d+\.?\d*,\d+\.?\d*$/;
29+
const IS_SG_COORDINATE_PAIR_REGEXP = /^[NS] *\d+° *\d+['] *\d+(?:"||\.\d+),? *[EW] *\d+° *\d+['] *\d+(?:"||\.\d+)|\d+° *\d+['] *\d+(?:"||\.\d+) *[NS],? *\d+° *\d+['] *\d+(?:"||\.\d+) *[EW]$/;
2630

2731
const NOMINATIM_RESPONSE_FORMAT = 'jsonv2';
2832

@@ -39,11 +43,11 @@ export class LocationService {
3943
) { }
4044

4145
/**
42-
* Search a place (address or POI) with Nominatim, and return the coordinates and the display name of the most relevant search result
46+
* Search Nominatim for a place (address or POI), then find the coordinates and the display name of the most relevant search result
4347
* @param address the place to be searched
4448
* @returns {LocationPlace} the information related to the searched place
4549
*/
46-
public searchPlace(address: string): Observable<LocationPlace> {
50+
public findPlaceCoordinates(address: string): Observable<LocationPlace> {
4751
let params = new HttpParams().append('q', address).append('format', NOMINATIM_RESPONSE_FORMAT);
4852

4953
return this.http.get<Record<string,any>[]>(this.searchEndpoint, { params: params }).pipe(
@@ -58,15 +62,15 @@ export class LocationService {
5862
}
5963
if (searchResults.length > 0) {
6064
const firstMatch = searchResults[0];
61-
const coordinates: LocationCoordinates = {
65+
const coordinates: LocationDDCoordinates = {
6266
latitude: parseFloat(firstMatch.lat),
6367
longitude: parseFloat(firstMatch.lon),
6468
};
65-
const info: LocationPlace = {
69+
const place: LocationPlace = {
6670
coordinates: coordinates,
6771
displayName: firstMatch.display_name,
6872
};
69-
return info;
73+
return place;
7074
} else {
7175
console.warn('Location service', `Location "${address}" not found`);
7276
throw Error(LocationErrorCodes.LOCATION_NOT_FOUND);
@@ -76,18 +80,53 @@ export class LocationService {
7680
}
7781

7882
/**
79-
* Search coordinates with Nominatim and return the display name
83+
* Search Nominatim for coordinates, then return the decimal coordinates and the display name
84+
* @param coordinates the coordinates, in any supported format
85+
* @returns {LocationPlace} the information related to the searched coordinates
86+
*/
87+
public findPlaceAndDecimalCoordinates(coordinates: string): Observable<LocationPlace> {
88+
let params = new HttpParams().append('q', coordinates).append('format', NOMINATIM_RESPONSE_FORMAT);
89+
90+
return this.http.get<Record<string,any>[]>(this.searchEndpoint, { params: params }).pipe(
91+
catchError((err) => {
92+
console.error('Location service', err);
93+
throw Error(LocationErrorCodes.API_ERROR);
94+
}),
95+
take(1),
96+
map((searchResults) => {
97+
if (searchResults.length > 0) {
98+
const firstMatch = searchResults[0];
99+
const decimalCoordinates: LocationDDCoordinates = {
100+
latitude: parseFloat(firstMatch.lat),
101+
longitude: parseFloat(firstMatch.lon),
102+
};
103+
const place: LocationPlace = {
104+
coordinates: decimalCoordinates,
105+
displayName: firstMatch.display_name,
106+
};
107+
return place;
108+
} else {
109+
console.warn('Location service', `Invalid coordinates ${coordinates}`);
110+
throw Error(LocationErrorCodes.INVALID_COORDINATES);
111+
}
112+
}),
113+
);
114+
}
115+
116+
/**
117+
* Search Nominatim for a place by coordinates, and return the display name
80118
* @param coordinates
81119
* @returns {Observable<string>} the display name for the coordinates
82120
*/
83-
public searchCoordinates(coordinates: LocationCoordinates): Observable<string> {
121+
public searchByCoordinates(coordinates: LocationDDCoordinates): Observable<string> {
84122
let params = new HttpParams()
85123
.append('lat', coordinates.latitude)
86124
.append('lon', coordinates.longitude)
87125
.append('format', NOMINATIM_RESPONSE_FORMAT);
88126

89127
return this.http.get<Record<string,any>>(this.reverseSearchEndpoint, { params: params }).pipe(
90128
catchError((err) => {
129+
console.error('Location service', err);
91130
throw Error(LocationErrorCodes.API_ERROR);
92131
}),
93132
take(1),
@@ -107,7 +146,7 @@ export class LocationService {
107146
* @param longitude the longitude as float number
108147
* @returns {boolean} whether the coordinates are valid
109148
*/
110-
public isValidCoordinatePair(latitude: number, longitude: number): boolean {
149+
public isValidDecimalCoordinatePair(latitude: number, longitude: number): boolean {
111150
return !(isNaN(latitude) || isNaN(longitude) || latitude > 90 || latitude < -90 || longitude > 180 || longitude < -180);
112151
}
113152

@@ -116,21 +155,30 @@ export class LocationService {
116155
* @param coordinateString the string to be checked
117156
* @returns {boolean} whether the string has a valid format
118157
*/
119-
public isCoordinateString(coordinateString: string): boolean {
120-
return IS_COORDINATE_PAIR_REGEXP.test(coordinateString) && coordinateString.split(',').length === 2;
158+
public isDecimalCoordinateString(coordinateString: string): boolean {
159+
return IS_DD_COORDINATE_PAIR_REGEXP.test(coordinateString) && coordinateString.split(',').length === 2;
160+
}
161+
162+
/**
163+
* Check if a string is in the format `latitude,longitude`
164+
* @param coordinateString the string to be checked
165+
* @returns {boolean} whether the string has a valid format
166+
*/
167+
public isSexagesimalCoordinateString(coordinateString: string): boolean {
168+
return IS_SG_COORDINATE_PAIR_REGEXP.test(coordinateString);
121169
}
122170

123171
/**
124172
* Check if a string contains valid coordinateString in the format `latitude,longitude`
125173
* @param coordinateString the string to be checked
126-
* @returns {boolean} whether the string is valid, and it contains valid coordinateString
174+
* @returns {boolean} whether the string is valid and it contains valid coordinateString
127175
*/
128176
public isValidCoordinateString(coordinateString: string): boolean {
129-
if (this.isCoordinateString(coordinateString)) {
177+
if (this.isDecimalCoordinateString(coordinateString)) {
130178
const coordinateArray = coordinateString.split(',');
131-
const latitude = parseFloat(coordinateArray[0]?.trim());
132-
const longitude = parseFloat(coordinateArray[1]?.trim());
133-
return !isNaN(latitude) && !isNaN(longitude) && this.isValidCoordinatePair(latitude, longitude);
179+
const latitude = parseFloat(coordinateArray[0]);
180+
const longitude = parseFloat(coordinateArray[1]);
181+
return !isNaN(latitude) && !isNaN(longitude) && this.isValidDecimalCoordinatePair(latitude, longitude);
134182
} else {
135183
return false;
136184
}
@@ -139,13 +187,13 @@ export class LocationService {
139187
/**
140188
* Parse a string containing location coordinates, and return an object containing latitude and longitude as numbers
141189
* @param coordinates a string in the format `latitude,longitude`
142-
* @returns {LocationCoordinates} the parsed coordinates
190+
* @returns {LocationDDCoordinates} the parsed coordinates
143191
*/
144-
public parseCoordinates(coordinates: string): LocationCoordinates {
192+
public parseCoordinates(coordinates: string): LocationDDCoordinates {
145193
const coordinateArr = coordinates.split(',');
146194
if (this.isValidCoordinateString(coordinates)) {
147-
const latitude = parseFloat(coordinateArr[0]?.trim());
148-
const longitude = parseFloat(coordinateArr[1]?.trim());
195+
const latitude = parseFloat(coordinateArr[0]);
196+
const longitude = parseFloat(coordinateArr[1]);
149197
return { latitude, longitude };
150198
} else {
151199
console.warn('Location service', `Invalid coordinates "${coordinates}"`);

src/app/shared/open-street-map/open-street-map.component.ts

Lines changed: 35 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,19 @@
11
import { Component, ElementRef, Input, OnInit } from '@angular/core';
22
import {
3-
LocationCoordinates,
3+
LocationDDCoordinates,
44
LocationErrorCodes,
55
LocationPlace,
66
LocationService
77
} from '../../core/services/location.service';
88
import { filter, map, tap } from 'rxjs/operators';
9-
import { BehaviorSubject, Observable, of } from 'rxjs';
9+
import { BehaviorSubject, Observable } from 'rxjs';
1010
import { TranslateService } from '@ngx-translate/core';
1111
import { isNotEmpty } from '../empty.util';
1212
import { latLng, LatLng, Layer, MapOptions, marker, tileLayer } from 'leaflet';
1313

1414

1515
export interface OpenStreetMapPointer {
16-
coordinates: LocationCoordinates,
16+
coordinates: LocationDDCoordinates,
1717
color: string,
1818
}
1919

@@ -65,7 +65,7 @@ export class OpenStreetMapComponent implements OnInit {
6565
/**
6666
* The coordinates of the place once retrieved by the location service
6767
*/
68-
coordinates$: Observable<LocationCoordinates>;
68+
coordinates$: Observable<LocationDDCoordinates>;
6969

7070
/**
7171
* The name of the address to display
@@ -135,13 +135,15 @@ export class OpenStreetMapComponent implements OnInit {
135135
map((place) => place.displayName),
136136
);
137137

138-
if (this.locationService.isCoordinateString(this.coordinates)) {
138+
const position = this.coordinates; // this may contain a pair or coordinates, a POI, or an address
139+
140+
if (this.locationService.isDecimalCoordinateString(position)) {
139141

140142
// Validate the coordinates, then retrieve the location name
141143

142-
if (this.locationService.isValidCoordinateString(this.coordinates)) {
143-
const coordinates = this.locationService.parseCoordinates(this.coordinates);
144-
this.locationService.searchCoordinates(coordinates).subscribe({
144+
if (this.locationService.isValidCoordinateString(position)) {
145+
const coordinates = this.locationService.parseCoordinates(position);
146+
this.locationService.searchByCoordinates(coordinates).subscribe({
145147
next: (displayName) => {
146148
const place: LocationPlace = {
147149
coordinates: coordinates,
@@ -163,17 +165,36 @@ export class OpenStreetMapComponent implements OnInit {
163165
},
164166
});
165167
} else {
166-
console.error(`Invalid coordinates: "${this.coordinates}"`);
168+
console.error(`Invalid coordinates: "${position}"`);
167169
this.invalidLocationErrorCode.next(LocationErrorCodes.INVALID_COORDINATES);
168170
}
169171

172+
} else if (this.locationService.isSexagesimalCoordinateString(position)) {
173+
174+
// Retrieve the decimal coordinates and the place name for the provided coordinates
175+
176+
this.locationService.findPlaceAndDecimalCoordinates(position).subscribe({
177+
next: (place) => {
178+
console.log('PLACE', place);
179+
this.place.next(place);
180+
},
181+
error: (err) => {
182+
this.invalidLocationErrorCode.next(err.message); // either INVALID_COORDINATES or API_ERROR
183+
if (err.message === LocationErrorCodes.API_ERROR) {
184+
console.error(err.message);
185+
} else {
186+
console.warn(err.message);
187+
}
188+
},
189+
});
190+
170191
} else {
171192

172193
// Retrieve the coordinates for the provided POI or address
173194

174-
this.locationService.searchPlace(this.coordinates).subscribe({
195+
this.locationService.findPlaceCoordinates(position).subscribe({
175196
next: (place) => {
176-
place.displayName = this.coordinates; // Show the name stored in metadata (comment out to show name retrieved from Nominatim)
197+
place.displayName = position; // Show the name stored in metadata (comment out to show name retrieved from Nominatim)
177198
this.place.next(place);
178199
},
179200
error: (err) => {
@@ -189,11 +210,11 @@ export class OpenStreetMapComponent implements OnInit {
189210

190211
}
191212

192-
private setCenterAndPointer(coordinates: LocationCoordinates) {
193-
this.leafletCenter = latLng(coordinates.latitude, coordinates.longitude);
213+
private setCenterAndPointer(coordinates: LocationDDCoordinates) {
214+
this.leafletCenter = latLng(+coordinates.latitude, +coordinates.longitude);
194215
this.leafletLayers = [
195216
tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {maxZoom: 18, attribution: 'Leaflet'}),
196-
marker([coordinates.latitude, coordinates.longitude])
217+
marker([+coordinates.latitude, +coordinates.longitude])
197218
];
198219
}
199220
}

0 commit comments

Comments
 (0)