Skip to content

Commit 7861e4c

Browse files
author
Davide Negretti
committed
Merged in UXP-120-maintenance (pull request DSpace#1444)
UXP-120 maintenance
2 parents c58eca9 + cfe592d commit 7861e4c

26 files changed

Lines changed: 1927 additions & 49 deletions

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,7 @@
143143
"ng2-nouislider": "^2.0.0",
144144
"ng2-pdfjs-viewer": "^15.0.0",
145145
"ngx-infinite-scroll": "^15.0.0",
146+
"ngx-openlayers": "0.8.22",
146147
"ngx-pagination": "6.0.3",
147148
"ngx-sortablejs": "^11.1.0",
148149
"ngx-ui-switch": "^14.0.3",
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { TestBed } from '@angular/core/testing';
2+
3+
import { LocationService } from './location.service';
4+
import { HttpClient } from '@angular/common/http';
5+
6+
describe('LocationService', () => {
7+
let service: LocationService;
8+
9+
beforeEach(() => {
10+
TestBed.configureTestingModule({
11+
providers: [
12+
{ provide: HttpClient, useValue: {} },
13+
],
14+
});
15+
service = TestBed.inject(LocationService);
16+
});
17+
18+
it('should be created', () => {
19+
expect(service).toBeTruthy();
20+
});
21+
22+
describe('Test utility methods', () => {
23+
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
37+
});
38+
39+
it('isValidCoordinateString() should validate coordinate strings and check for their values correctly', () => {
40+
expect(service.isValidCoordinateString('45,45')).toBeTrue();
41+
expect(service.isValidCoordinateString('200,200')).toBeFalse(); // valid pattern, invalid numbers
42+
});
43+
44+
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();
54+
});
55+
});
56+
});
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
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 { catchError, 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+
API_ERROR = 'api-error',
23+
}
24+
25+
const IS_COORDINATE_PAIR_REGEXP = /^\d+\.?\d*,\d+\.?\d*$/;
26+
27+
const NOMINATIM_RESPONSE_FORMAT = 'jsonv2';
28+
29+
@Injectable({
30+
providedIn: 'root'
31+
})
32+
export class LocationService {
33+
34+
searchEndpoint = environment.location.nominatimApi.searchEndpoint;
35+
reverseSearchEndpoint = environment.location.nominatimApi.reverseSearchEndpoint;
36+
37+
constructor(
38+
protected http: HttpClient,
39+
) { }
40+
41+
/**
42+
* Search a place (address or POI) with Nominatim, and return the coordinates and the display name of the most relevant search result
43+
* @param address the place to be searched
44+
* @returns {LocationPlace} the information related to the searched place
45+
*/
46+
public searchPlace(address: string): Observable<LocationPlace> {
47+
let params = new HttpParams().append('q', address).append('format', NOMINATIM_RESPONSE_FORMAT);
48+
49+
return this.http.get<Record<string,any>[]>(this.searchEndpoint, { params: params }).pipe(
50+
catchError((err) => {
51+
console.error('Location service', err);
52+
throw Error(LocationErrorCodes.API_ERROR);
53+
}),
54+
take(1),
55+
map((searchResults) => {
56+
if (searchResults.length > 1) {
57+
console.warn('Location service', `Multiple locations found for address "${address}"`, 'Showing top matches', searchResults.slice(0,5));
58+
}
59+
if (searchResults.length > 0) {
60+
const firstMatch = searchResults[0];
61+
const coordinates: LocationCoordinates = {
62+
latitude: parseFloat(firstMatch.lat),
63+
longitude: parseFloat(firstMatch.lon),
64+
};
65+
const info: LocationPlace = {
66+
coordinates: coordinates,
67+
displayName: firstMatch.display_name,
68+
};
69+
return info;
70+
} else {
71+
console.warn('Location service', `Location "${address}" not found`);
72+
throw Error(LocationErrorCodes.LOCATION_NOT_FOUND);
73+
}
74+
}),
75+
);
76+
}
77+
78+
/**
79+
* Search coordinates with Nominatim and return the display name
80+
* @param coordinates
81+
* @returns {Observable<string>} the display name for the coordinates
82+
*/
83+
public searchCoordinates(coordinates: LocationCoordinates): Observable<string> {
84+
let params = new HttpParams()
85+
.append('lat', coordinates.latitude)
86+
.append('lon', coordinates.longitude)
87+
.append('format', NOMINATIM_RESPONSE_FORMAT);
88+
89+
return this.http.get<Record<string,any>>(this.reverseSearchEndpoint, { params: params }).pipe(
90+
catchError((err) => {
91+
throw Error(LocationErrorCodes.API_ERROR);
92+
}),
93+
take(1),
94+
map((searchResults) => {
95+
if (hasValue(searchResults.error)) {
96+
throw Error(searchResults.error);
97+
} else {
98+
return searchResults.display_name;
99+
}
100+
}),
101+
);
102+
}
103+
104+
/**
105+
* Check if the provided pair of coordinates is valid
106+
* @param latitude the latitude as float number
107+
* @param longitude the longitude as float number
108+
* @returns {boolean} whether the coordinates are valid
109+
*/
110+
public isValidCoordinatePair(latitude: number, longitude: number): boolean {
111+
return !(isNaN(latitude) || isNaN(longitude) || latitude > 90 || latitude < -90 || longitude > 180 || longitude < -180);
112+
}
113+
114+
/**
115+
* Check if a string is in the format `latitude,longitude`
116+
* @param coordinateString the string to be checked
117+
* @returns {boolean} whether the string has a valid format
118+
*/
119+
public isCoordinateString(coordinateString: string): boolean {
120+
return IS_COORDINATE_PAIR_REGEXP.test(coordinateString) && coordinateString.split(',').length === 2;
121+
}
122+
123+
/**
124+
* Check if a string contains valid coordinateString in the format `latitude,longitude`
125+
* @param coordinateString the string to be checked
126+
* @returns {boolean} whether the string is valid and it contains valid coordinateString
127+
*/
128+
public isValidCoordinateString(coordinateString: string): boolean {
129+
if (this.isCoordinateString(coordinateString)) {
130+
const coordinateArray = coordinateString.split(',');
131+
const latitude = parseFloat(coordinateArray[0]);
132+
const longitude = parseFloat(coordinateArray[1]);
133+
return !isNaN(latitude) && !isNaN(longitude) && this.isValidCoordinatePair(latitude, longitude);
134+
} else {
135+
return false;
136+
}
137+
}
138+
139+
/**
140+
* Parse a string containing location coordinates, and return an object containing latitude and longitude as numbers
141+
* @param coordinates a string in the format `latitude,longitude`
142+
* @returns {LocationCoordinates} the parsed coordinates
143+
*/
144+
public parseCoordinates(coordinates: string): LocationCoordinates {
145+
const coordinateArr = coordinates.split(',');
146+
if (this.isValidCoordinateString(coordinates)) {
147+
const latitude = parseFloat(coordinateArr[0]);
148+
const longitude = parseFloat(coordinateArr[1]);
149+
return { latitude, longitude };
150+
} else {
151+
console.warn('Location service', `Invalid coordinates "${coordinates}"`);
152+
throw Error(LocationErrorCodes.INVALID_COORDINATES);
153+
}
154+
}
155+
}

src/app/cris-layout/cris-layout-matrix/cris-layout-box-container/boxes/metadata/rendering-types/map/map.component.html renamed to src/app/cris-layout/cris-layout-matrix/cris-layout-box-container/boxes/metadata/rendering-types/gmap/gmap.component.html

File renamed without changes.

src/app/cris-layout/cris-layout-matrix/cris-layout-box-container/boxes/metadata/rendering-types/map/map.component.scss renamed to src/app/cris-layout/cris-layout-matrix/cris-layout-box-container/boxes/metadata/rendering-types/gmap/gmap.component.scss

File renamed without changes.

src/app/cris-layout/cris-layout-matrix/cris-layout-box-container/boxes/metadata/rendering-types/map/map.component.spec.ts renamed to src/app/cris-layout/cris-layout-matrix/cris-layout-box-container/boxes/metadata/rendering-types/gmap/gmap.component.spec.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,11 @@ import { LayoutField } from '../../../../../../../core/layout/models/box.model';
66
import { Item } from '../../../../../../../core/shared/item.model';
77
import { MetadataValue } from '../../../../../../../core/shared/metadata.models';
88
import { TranslateLoaderMock } from '../../../../../../../shared/mocks/translate-loader.mock';
9-
import { MapComponent } from './map.component';
9+
import { GmapComponent } from './gmap.component';
1010

11-
describe('MapComponent', () => {
12-
let component: MapComponent;
13-
let fixture: ComponentFixture<MapComponent>;
11+
describe('GmapComponent', () => {
12+
let component: GmapComponent;
13+
let fixture: ComponentFixture<GmapComponent>;
1414

1515
const metadataValue = Object.assign(new MetadataValue(), {
1616
'value': '@42.1334,56.7654',
@@ -33,7 +33,7 @@ describe('MapComponent', () => {
3333
const mockField: LayoutField = {
3434
'metadata': 'organization.address.addressLocality',
3535
'label': 'Preferred name',
36-
'rendering': 'MAP',
36+
'rendering': 'GOOGLEMAPS',
3737
'fieldType': 'METADATA',
3838
'style': null,
3939
'styleLabel': 'test-style-label',
@@ -56,13 +56,13 @@ describe('MapComponent', () => {
5656
{ provide: 'metadataValueProvider', useValue: metadataValue },
5757
{ provide: 'renderingSubTypeProvider', useValue: '' },
5858
],
59-
declarations: [ MapComponent ]
59+
declarations: [ GmapComponent ]
6060
})
6161
.compileComponents();
6262
});
6363

6464
beforeEach(() => {
65-
fixture = TestBed.createComponent(MapComponent);
65+
fixture = TestBed.createComponent(GmapComponent);
6666
component = fixture.componentInstance;
6767
fixture.detectChanges();
6868
});

src/app/cris-layout/cris-layout-matrix/cris-layout-box-container/boxes/metadata/rendering-types/map/map.component.ts renamed to src/app/cris-layout/cris-layout-matrix/cris-layout-box-container/boxes/metadata/rendering-types/gmap/gmap.component.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,12 @@ import { FieldRenderingType, MetadataBoxFieldRendering, } from '../metadata-box.
44
import { RenderingTypeValueModelComponent } from '../rendering-type-value.model';
55

66
@Component({
7-
selector: 'ds-map',
8-
templateUrl: './map.component.html',
9-
styleUrls: ['./map.component.scss'],
7+
selector: 'ds-gmap',
8+
templateUrl: './gmap.component.html',
9+
styleUrls: ['./gmap.component.scss'],
1010
})
11-
@MetadataBoxFieldRendering(FieldRenderingType.MAP)
12-
export class MapComponent extends RenderingTypeValueModelComponent implements OnInit {
11+
@MetadataBoxFieldRendering(FieldRenderingType.GMAP)
12+
export class GmapComponent extends RenderingTypeValueModelComponent implements OnInit {
1313
coordinates: string;
1414
ngOnInit(): void {
1515
this.coordinates = this.metadataValue.value;

src/app/cris-layout/cris-layout-matrix/cris-layout-box-container/boxes/metadata/rendering-types/metadata-box.decorator.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ export enum FieldRenderingType {
1717
TAG = 'TAG',
1818
VALUEPAIR = 'VALUEPAIR',
1919
HTML = 'HTML',
20-
MAP = 'MAP',
20+
GMAP = 'GOOGLEMAPS',
21+
OSMAP = 'OPENSTREETMAP',
2122
BROWSE = 'BROWSE',
2223
TAGBROWSE = 'TAG-BROWSE',
2324
MARKDOWN = 'MARKDOWN',
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<div *ngIf="(place | async )">
2+
<ds-open-street-map
3+
[coordinates]="(coordinates$ | async)"
4+
[pointers]="(pointers$ | async)"
5+
[showControlsZoom]="true"
6+
></ds-open-street-map>
7+
<div class="my-1 text-muted">{{ displayName$ | async }}</div>
8+
</div>
9+
10+
<div *ngIf="(invalidLocationErrorCode | async) as errorCode" class="text-danger">{{ 'location.error.' + errorCode | translate }}</div>

src/app/cris-layout/cris-layout-matrix/cris-layout-box-container/boxes/metadata/rendering-types/open-street-map/open-street-map-rendering.component.scss

Whitespace-only changes.

0 commit comments

Comments
 (0)