Skip to content
This repository was archived by the owner on Jun 23, 2025. It is now read-only.

Commit 3549ccf

Browse files
DefJunxdoom777
authored andcommitted
feat(AgmGeocoder): add geocoder service and tests (#1743)
- add geocoder service - add interfaces to support geocoder request and response - add test cases Implements: #1694
1 parent c16e666 commit 3549ccf

File tree

5 files changed

+277
-0
lines changed

5 files changed

+277
-0
lines changed

packages/core/map-types.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,18 @@ export {
66
DataMouseEvent,
77
LatLngBounds,
88
LatLngBoundsLiteral,
9+
LatLng,
910
LatLngLiteral,
1011
PolyMouseEvent,
1112
MarkerLabel,
13+
Geocoder,
14+
GeocoderAddressComponent,
15+
GeocoderComponentRestrictions,
16+
GeocoderGeometry,
17+
GeocoderLocationType,
18+
GeocoderRequest,
19+
GeocoderResult,
20+
GeocoderStatus,
1221
} from './services/google-maps-types';
1322

1423
/**

packages/core/services.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
export { CircleManager } from './services/managers/circle-manager';
22
export { DataLayerManager } from './services/managers/data-layer-manager';
33
export { FitBoundsAccessor, FitBoundsDetails } from './services/fit-bounds';
4+
export { AgmGeocoder } from './services/geocoder-service';
45
export { GoogleMapsAPIWrapper } from './services/google-maps-api-wrapper';
56
export {
67
GoogleMapsScriptProtocol,
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
import { discardPeriodicTasks, fakeAsync, TestBed, tick } from '@angular/core/testing';
2+
3+
import { AgmGeocoder } from './geocoder-service';
4+
import { MapsAPILoader } from './maps-api-loader/maps-api-loader';
5+
6+
describe('GeocoderService', () => {
7+
let loader: MapsAPILoader;
8+
let geocoderService: AgmGeocoder;
9+
let geocoderConstructs: number;
10+
let geocodeMock: jest.Mock;
11+
12+
beforeEach(fakeAsync(() => {
13+
loader = {
14+
load: jest.fn().mockReturnValue(Promise.resolve()),
15+
};
16+
17+
geocoderConstructs = 0;
18+
geocodeMock = jest.fn();
19+
20+
(window as any).google = {
21+
maps: {
22+
Geocoder: class Geocoder {
23+
geocode: jest.Mock = geocodeMock;
24+
25+
constructor() {
26+
geocoderConstructs += 1;
27+
}
28+
},
29+
},
30+
};
31+
32+
TestBed.configureTestingModule({
33+
providers: [
34+
{ provide: MapsAPILoader, useValue: loader },
35+
AgmGeocoder,
36+
],
37+
});
38+
39+
geocoderService = TestBed.get(AgmGeocoder);
40+
tick();
41+
}));
42+
43+
it('should wait for the load event', () => {
44+
expect(loader.load).toHaveBeenCalledTimes(1);
45+
expect(geocoderConstructs).toEqual(1);
46+
});
47+
48+
it('should emit a geocode result', fakeAsync(() => {
49+
const success = jest.fn();
50+
const geocodeRequest = {
51+
address: 'Mountain View, California, United States',
52+
};
53+
const geocodeExampleResponse = {
54+
'results': [
55+
{
56+
'address_components': [
57+
{
58+
'long_name': '1600',
59+
'short_name': '1600',
60+
'types': ['street_number'],
61+
},
62+
{
63+
'long_name': 'Amphitheatre Parkway',
64+
'short_name': 'Amphitheatre Pkwy',
65+
'types': ['route'],
66+
},
67+
{
68+
'long_name': 'Mountain View',
69+
'short_name': 'Mountain View',
70+
'types': ['locality', 'political'],
71+
},
72+
{
73+
'long_name': 'Santa Clara County',
74+
'short_name': 'Santa Clara County',
75+
'types': ['administrative_area_level_2', 'political'],
76+
},
77+
{
78+
'long_name': 'California',
79+
'short_name': 'CA',
80+
'types': ['administrative_area_level_1', 'political'],
81+
},
82+
{
83+
'long_name': 'United States',
84+
'short_name': 'US',
85+
'types': ['country', 'political'],
86+
},
87+
{
88+
'long_name': '94043',
89+
'short_name': '94043',
90+
'types': ['postal_code'],
91+
},
92+
],
93+
'formatted_address': '1600 Amphitheatre Pkwy, Mountain View, CA 94043, USA',
94+
'geometry': {
95+
'location': {
96+
'lat': 37.4267861,
97+
'lng': -122.0806032,
98+
},
99+
'location_type': 'ROOFTOP',
100+
'viewport': {
101+
'northeast': {
102+
'lat': 37.4281350802915,
103+
'lng': -122.0792542197085,
104+
},
105+
'southwest': {
106+
'lat': 37.4254371197085,
107+
'lng': -122.0819521802915,
108+
},
109+
},
110+
},
111+
'place_id': 'ChIJtYuu0V25j4ARwu5e4wwRYgE',
112+
'plus_code': {
113+
'compound_code': 'CWC8+R3 Mountain View, California, United States',
114+
'global_code': '849VCWC8+R3',
115+
},
116+
'types': ['street_address'],
117+
},
118+
],
119+
'status': 'OK',
120+
};
121+
122+
geocodeMock.mockImplementation((_geocodeRequest, callback) => callback(geocodeExampleResponse, 'OK'));
123+
124+
geocoderService.geocode(geocodeRequest).subscribe(success);
125+
126+
tick();
127+
128+
expect(success).toHaveBeenCalledTimes(1);
129+
expect(success).toHaveBeenCalledWith(geocodeExampleResponse);
130+
expect(geocodeMock).toHaveBeenCalledTimes(1);
131+
132+
discardPeriodicTasks();
133+
}));
134+
135+
it('should catch error if Google does not return a OK result', fakeAsync(() => {
136+
const success = jest.fn();
137+
const catchFn = jest.fn();
138+
const geocodeRequest = {
139+
address: 'Mountain View, California, United States',
140+
};
141+
142+
geocodeMock.mockImplementation((geocodeRequest, callback) => callback(geocodeRequest, 'INVALID_REQUEST'));
143+
144+
geocoderService.geocode(geocodeRequest).subscribe(success, catchFn);
145+
146+
tick();
147+
148+
expect(success).toHaveBeenCalledTimes(0);
149+
expect(catchFn).toHaveBeenCalledTimes(1);
150+
expect(catchFn).toHaveBeenCalledWith('INVALID_REQUEST');
151+
expect(geocodeMock).toHaveBeenCalledTimes(1);
152+
153+
discardPeriodicTasks();
154+
}));
155+
});
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { Injectable } from '@angular/core';
2+
import { bindCallback, ConnectableObservable, Observable, of, ReplaySubject, throwError } from 'rxjs';
3+
import { map, multicast, switchMap } from 'rxjs/operators';
4+
import { Geocoder, GeocoderRequest, GeocoderResult, GeocoderStatus } from './google-maps-types';
5+
import { MapsAPILoader } from './maps-api-loader/maps-api-loader';
6+
7+
declare var google: any;
8+
9+
@Injectable({ providedIn: 'root' })
10+
export class AgmGeocoder {
11+
protected readonly geocoder$: Observable<Geocoder>;
12+
13+
constructor(loader: MapsAPILoader) {
14+
const connectableGeocoder$ = new Observable(subscriber => {
15+
loader.load().then(() => subscriber.next());
16+
})
17+
.pipe(
18+
map(() => this._createGeocoder()),
19+
multicast(new ReplaySubject(1)),
20+
) as ConnectableObservable<Geocoder>;
21+
22+
connectableGeocoder$.connect(); // ignore the subscription
23+
// since we will remain subscribed till application exits
24+
25+
this.geocoder$ = connectableGeocoder$;
26+
}
27+
28+
geocode(request: GeocoderRequest): Observable<GeocoderResult[]> {
29+
return this.geocoder$.pipe(
30+
switchMap((geocoder) => this._getGoogleResults(geocoder, request))
31+
);
32+
}
33+
34+
private _getGoogleResults(geocoder: Geocoder, request: GeocoderRequest): Observable<GeocoderResult[]> {
35+
const geocodeObservable = bindCallback(geocoder.geocode);
36+
return geocodeObservable(request).pipe(
37+
switchMap(([results, status]) => {
38+
if (status === GeocoderStatus.OK) {
39+
return of(results);
40+
}
41+
42+
return throwError(status);
43+
})
44+
);
45+
}
46+
47+
private _createGeocoder(): Geocoder {
48+
return new google.maps.Geocoder() as Geocoder;
49+
}
50+
}

packages/core/services/google-maps-types.ts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -634,3 +634,65 @@ export interface MapRestriction {
634634
latLngBounds: LatLngBounds | LatLngBoundsLiteral;
635635
strictBounds?: boolean;
636636
}
637+
638+
export interface Geocoder {
639+
geocode: (request: GeocoderRequest, googleCallback: (results: GeocoderResult[], status: GeocoderStatus) => void) => void;
640+
}
641+
642+
export interface GeocoderAddressComponent {
643+
long_name: string;
644+
short_name: string;
645+
types: string[];
646+
}
647+
648+
/** Options for restricting the geocoder results */
649+
export interface GeocoderComponentRestrictions {
650+
administrativeArea?: string;
651+
country?: string;
652+
locality?: string;
653+
postalCode?: string;
654+
route?: string;
655+
}
656+
657+
export interface GeocoderGeometry {
658+
bounds: LatLngBounds;
659+
location: LatLng;
660+
location_type: GeocoderLocationType;
661+
viewport: LatLngBounds;
662+
}
663+
664+
export enum GeocoderLocationType {
665+
APPROXIMATE = 'APPROXIMATE',
666+
GEOMETRIC_CENTER = 'GEOMETRIC_CENTER',
667+
RANGE_INTERPOLATED = 'RANGE_INTERPOLATED',
668+
ROOFTOP = 'ROOFTOP',
669+
}
670+
671+
export interface GeocoderRequest {
672+
address?: string;
673+
bounds?: LatLngBounds | LatLngBoundsLiteral;
674+
componentRestrictions?: GeocoderComponentRestrictions;
675+
location?: LatLng | LatLngLiteral;
676+
placeId?: string;
677+
region?: string;
678+
}
679+
680+
export interface GeocoderResult {
681+
address_components: GeocoderAddressComponent[];
682+
formatted_address: string;
683+
geometry: GeocoderGeometry;
684+
partial_match: boolean;
685+
place_id: string;
686+
postcode_localities: string[];
687+
types: string[];
688+
}
689+
690+
export enum GeocoderStatus {
691+
ERROR = 'ERROR',
692+
INVALID_REQUEST = 'INVALID_REQUEST',
693+
OK = 'OK',
694+
OVER_QUERY_LIMIT = 'OVER_QUERY_LIMIT',
695+
REQUEST_DENIED = 'REQUEST_DENIED',
696+
UNKNOWN_ERROR = 'UNKNOWN_ERROR',
697+
ZERO_RESULTS = 'ZERO_RESULTS',
698+
}

0 commit comments

Comments
 (0)