Skip to content

Commit

Permalink
feat(AgmGeocoder): add geocoder service and tests (#1743)
Browse files Browse the repository at this point in the history
- add geocoder service
- add interfaces to support geocoder request and response
- add test cases

Implements: #1694
  • Loading branch information
DefJunx authored and doom777 committed Oct 18, 2019
1 parent c16e666 commit 3549ccf
Show file tree
Hide file tree
Showing 5 changed files with 277 additions and 0 deletions.
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);
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 {
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',
}

0 comments on commit 3549ccf

Please sign in to comment.