Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(AgmGeocoder): add geocoder service and tests #1743

Merged
14 commits merged into from
Oct 18, 2019
9 changes: 9 additions & 0 deletions packages/core/map-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,18 @@ export {
DataMouseEvent,
LatLngBounds,
LatLngBoundsLiteral,
LatLng,
LatLngLiteral,
PolyMouseEvent,
MarkerLabel,
Geocoder,
GeocoderAddressComponent,
GeocoderComponentRestrictions,
GeocoderGeometry,
GeocoderLocationType,
GeocoderRequest,
GeocoderResult,
GeocoderStatus,
} from './services/google-maps-types';

/**
Expand Down
1 change: 1 addition & 0 deletions packages/core/services.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export { CircleManager } from './services/managers/circle-manager';
export { DataLayerManager } from './services/managers/data-layer-manager';
export { FitBoundsAccessor, FitBoundsDetails } from './services/fit-bounds';
export { AgmGeocoder } from './services/geocoder-service';
export { GoogleMapsAPIWrapper } from './services/google-maps-api-wrapper';
export {
GoogleMapsScriptProtocol,
Expand Down
155 changes: 155 additions & 0 deletions packages/core/services/geocoder-service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
import { discardPeriodicTasks, fakeAsync, TestBed, tick } from '@angular/core/testing';

import { AgmGeocoder } from './geocoder-service';
import { MapsAPILoader } from './maps-api-loader/maps-api-loader';

describe('GeocoderService', () => {
let loader: MapsAPILoader;
let geocoderService: AgmGeocoder;
let geocoderConstructs: number;
let geocodeMock: jest.Mock;

beforeEach(fakeAsync(() => {
loader = {
load: jest.fn().mockReturnValue(Promise.resolve()),
};

geocoderConstructs = 0;
geocodeMock = jest.fn();

(window as any).google = {
maps: {
Geocoder: class Geocoder {
geocode: jest.Mock = geocodeMock;

constructor() {
geocoderConstructs += 1;
}
},
},
};

TestBed.configureTestingModule({
providers: [
{ provide: MapsAPILoader, useValue: loader },
AgmGeocoder,
],
});

geocoderService = TestBed.get(AgmGeocoder);
tick();
}));

it('should wait for the load event', () => {
expect(loader.load).toHaveBeenCalledTimes(1);
expect(geocoderConstructs).toEqual(1);
});

it('should emit a geocode result', fakeAsync(() => {
const success = jest.fn();
const geocodeRequest = {
address: 'Mountain View, California, United States',
};
const geocodeExampleResponse = {
'results': [
{
'address_components': [
{
'long_name': '1600',
'short_name': '1600',
'types': ['street_number'],
},
{
'long_name': 'Amphitheatre Parkway',
'short_name': 'Amphitheatre Pkwy',
'types': ['route'],
},
{
'long_name': 'Mountain View',
'short_name': 'Mountain View',
'types': ['locality', 'political'],
},
{
'long_name': 'Santa Clara County',
'short_name': 'Santa Clara County',
'types': ['administrative_area_level_2', 'political'],
},
{
'long_name': 'California',
'short_name': 'CA',
'types': ['administrative_area_level_1', 'political'],
},
{
'long_name': 'United States',
'short_name': 'US',
'types': ['country', 'political'],
},
{
'long_name': '94043',
'short_name': '94043',
'types': ['postal_code'],
},
],
'formatted_address': '1600 Amphitheatre Pkwy, Mountain View, CA 94043, USA',
'geometry': {
'location': {
'lat': 37.4267861,
'lng': -122.0806032,
},
'location_type': 'ROOFTOP',
'viewport': {
'northeast': {
'lat': 37.4281350802915,
'lng': -122.0792542197085,
},
'southwest': {
'lat': 37.4254371197085,
'lng': -122.0819521802915,
},
},
},
'place_id': 'ChIJtYuu0V25j4ARwu5e4wwRYgE',
'plus_code': {
'compound_code': 'CWC8+R3 Mountain View, California, United States',
'global_code': '849VCWC8+R3',
},
'types': ['street_address'],
},
],
'status': 'OK',
};

geocodeMock.mockImplementation((_geocodeRequest, callback) => callback(geocodeExampleResponse, 'OK'));

geocoderService.geocode(geocodeRequest).subscribe(success);

tick();

expect(success).toHaveBeenCalledTimes(1);
DefJunx marked this conversation as resolved.
Show resolved Hide resolved
expect(success).toHaveBeenCalledWith(geocodeExampleResponse);
expect(geocodeMock).toHaveBeenCalledTimes(1);

discardPeriodicTasks();
}));

it('should catch error if Google does not return a OK result', fakeAsync(() => {
const success = jest.fn();
const catchFn = jest.fn();
const geocodeRequest = {
address: 'Mountain View, California, United States',
};

geocodeMock.mockImplementation((geocodeRequest, callback) => callback(geocodeRequest, 'INVALID_REQUEST'));

geocoderService.geocode(geocodeRequest).subscribe(success, catchFn);

tick();

expect(success).toHaveBeenCalledTimes(0);
expect(catchFn).toHaveBeenCalledTimes(1);
expect(catchFn).toHaveBeenCalledWith('INVALID_REQUEST');
expect(geocodeMock).toHaveBeenCalledTimes(1);

discardPeriodicTasks();
}));
});
50 changes: 50 additions & 0 deletions packages/core/services/geocoder-service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { Injectable } from '@angular/core';
import { bindCallback, ConnectableObservable, Observable, of, ReplaySubject, throwError } from 'rxjs';
import { map, multicast, switchMap } from 'rxjs/operators';
import { Geocoder, GeocoderRequest, GeocoderResult, GeocoderStatus } from './google-maps-types';
import { MapsAPILoader } from './maps-api-loader/maps-api-loader';

declare var google: any;

@Injectable({ providedIn: 'root' })
export class AgmGeocoder {
protected readonly geocoder$: Observable<Geocoder>;

constructor(loader: MapsAPILoader) {
const connectableGeocoder$ = new Observable(subscriber => {
loader.load().then(() => subscriber.next());
})
.pipe(
map(() => this._createGeocoder()),
multicast(new ReplaySubject(1)),
) as ConnectableObservable<Geocoder>;

connectableGeocoder$.connect(); // ignore the subscription
// since we will remain subscribed till application exits

this.geocoder$ = connectableGeocoder$;
}

geocode(request: GeocoderRequest): Observable<GeocoderResult[]> {
return this.geocoder$.pipe(
switchMap((geocoder) => this._getGoogleResults(geocoder, request))
);
}

private _getGoogleResults(geocoder: Geocoder, request: GeocoderRequest): Observable<GeocoderResult[]> {
const geocodeObservable = bindCallback(geocoder.geocode);
return geocodeObservable(request).pipe(
switchMap(([results, status]) => {
if (status === GeocoderStatus.OK) {
return of(results);
}

return throwError(status);
})
);
}

private _createGeocoder(): Geocoder {
return new google.maps.Geocoder() as Geocoder;
}
}
62 changes: 62 additions & 0 deletions packages/core/services/google-maps-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -634,3 +634,65 @@ export interface MapRestriction {
latLngBounds: LatLngBounds | LatLngBoundsLiteral;
strictBounds?: boolean;
}

export interface Geocoder {
DefJunx marked this conversation as resolved.
Show resolved Hide resolved
geocode: (request: GeocoderRequest, googleCallback: (results: GeocoderResult[], status: GeocoderStatus) => void) => void;
}

export interface GeocoderAddressComponent {
long_name: string;
short_name: string;
types: string[];
}

/** Options for restricting the geocoder results */
export interface GeocoderComponentRestrictions {
administrativeArea?: string;
country?: string;
locality?: string;
postalCode?: string;
route?: string;
}

export interface GeocoderGeometry {
bounds: LatLngBounds;
location: LatLng;
location_type: GeocoderLocationType;
viewport: LatLngBounds;
}

export enum GeocoderLocationType {
APPROXIMATE = 'APPROXIMATE',
GEOMETRIC_CENTER = 'GEOMETRIC_CENTER',
RANGE_INTERPOLATED = 'RANGE_INTERPOLATED',
ROOFTOP = 'ROOFTOP',
}

export interface GeocoderRequest {
address?: string;
bounds?: LatLngBounds | LatLngBoundsLiteral;
componentRestrictions?: GeocoderComponentRestrictions;
location?: LatLng | LatLngLiteral;
placeId?: string;
region?: string;
}

export interface GeocoderResult {
address_components: GeocoderAddressComponent[];
formatted_address: string;
geometry: GeocoderGeometry;
partial_match: boolean;
place_id: string;
postcode_localities: string[];
types: string[];
}

export enum GeocoderStatus {
ERROR = 'ERROR',
INVALID_REQUEST = 'INVALID_REQUEST',
OK = 'OK',
OVER_QUERY_LIMIT = 'OVER_QUERY_LIMIT',
REQUEST_DENIED = 'REQUEST_DENIED',
UNKNOWN_ERROR = 'UNKNOWN_ERROR',
ZERO_RESULTS = 'ZERO_RESULTS',
}