Skip to content

Commit 86251af

Browse files
authored
feat(google-maps): add geocoder wrapper (#21832)
Adds a wrapper around the Google Maps Geocoder API. Also exports `MapDirectionsResponse` which wasn't exposed in a previous PR. Fixes #21665.
1 parent b92f97f commit 86251af

File tree

7 files changed

+178
-1
lines changed

7 files changed

+178
-1
lines changed

src/google-maps/README.md

+5
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,11 @@ export class GoogleMapsDemoComponent {
113113
- [`MapDirectionsRenderer`](./map-directions-renderer/README.md)
114114
- [`MapHeatmapLayer`](./map-heatmap-layer/README.md)
115115

116+
## Services
117+
118+
- [`MapGeocoder`](./map-geocoder/README.md)
119+
120+
116121
## The Options Input
117122

118123
The Google Maps components implement all of the options for their respective objects from the
+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# MapGeocoder
2+
3+
The `MapGeocoder`, like the `google.maps.Geocoder`, has a single method, `geocode`. Normally, the
4+
`google.maps.Geocoder` takes two arguments, a `google.maps.GeocoderRequest` and a callback that
5+
takes the `google.maps.GeocoderResult` and `google.maps.GeocoderStatus` as arguments.
6+
The `MapGeocoder.geocode` method takes takes the `google.maps.GeocoderRequest` as the single
7+
argument, and returns an `Observable` of a `MapGeocoderResponse`, which is an interface defined as
8+
follows:
9+
10+
```typescript
11+
export interface MapGeocoderResponse {
12+
status: google.maps.GeocoderStatus;
13+
results: google.maps.GeocoderResult[];
14+
}
15+
```
16+
17+
## Loading the Library
18+
19+
Using the `MapGeocoder` requires the Geocoding API to be enabled in Google Cloud Console on the
20+
same project as the one set up for the Google Maps JavaScript API, and requires an API key that
21+
has billing enabled. See [here](https://developers.google.com/maps/documentation/javascript/geocoding#GetStarted) for details.
22+
23+
## Example
24+
25+
```typescript
26+
// google-maps-demo.component.ts
27+
import {Component} from '@angular/core';
28+
import {MapGeocoder} from '@angular/google-maps';
29+
30+
@Component({
31+
selector: 'google-map-demo',
32+
templateUrl: 'google-map-demo.html',
33+
})
34+
export class GoogleMapDemo {
35+
constructor(geocoder: MapGeocoder) {
36+
geocoder.geocode({
37+
address: '1600 Amphitheatre Parkway, Mountain View, CA'
38+
}).subscribe(({results}) => {
39+
console.log(results);
40+
});
41+
}
42+
}
43+
```
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import {TestBed} from '@angular/core/testing';
2+
import {MapGeocoderResponse, MapGeocoder} from './map-geocoder';
3+
import {GoogleMapsModule} from '../google-maps-module';
4+
import {createGeocoderConstructorSpy, createGeocoderSpy} from '../testing/fake-google-map-utils';
5+
6+
describe('MapGeocoder', () => {
7+
let geocoder: MapGeocoder;
8+
let geocoderConstructorSpy: jasmine.Spy;
9+
let geocoderSpy: jasmine.SpyObj<google.maps.Geocoder>;
10+
11+
beforeEach(() => {
12+
TestBed.configureTestingModule({
13+
imports: [GoogleMapsModule],
14+
});
15+
16+
geocoderSpy = createGeocoderSpy();
17+
geocoderConstructorSpy = createGeocoderConstructorSpy(geocoderSpy).and.callThrough();
18+
geocoder = TestBed.inject(MapGeocoder);
19+
});
20+
21+
afterEach(() => {
22+
(window.google as any) = undefined;
23+
});
24+
25+
it('initializes the Google Maps Geocoder', () => {
26+
expect(geocoderConstructorSpy).toHaveBeenCalled();
27+
});
28+
29+
it('calls geocode on inputs', () => {
30+
const results: google.maps.GeocoderResult[] = [];
31+
const status = 'OK';
32+
geocoderSpy.geocode.and.callFake((_: google.maps.GeocoderRequest, callback: Function) => {
33+
callback(results, status);
34+
});
35+
const request: google.maps.DirectionsRequest = {};
36+
geocoder.geocode(request).subscribe(response => {
37+
expect(response).toEqual({results, status} as MapGeocoderResponse);
38+
});
39+
});
40+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
// Workaround for: https://github.com/bazelbuild/rules_nodejs/issues/1265
10+
/// <reference types="googlemaps" />
11+
12+
import {Injectable, NgZone} from '@angular/core';
13+
import {Observable} from 'rxjs';
14+
15+
export interface MapGeocoderResponse {
16+
status: google.maps.GeocoderStatus;
17+
results: google.maps.GeocoderResult[];
18+
}
19+
20+
/**
21+
* Angular service that wraps the Google Maps Geocoder from the Google Maps JavaScript API.
22+
* See developers.google.com/maps/documentation/javascript/reference/geocoder#Geocoder
23+
*/
24+
@Injectable({providedIn: 'root'})
25+
export class MapGeocoder {
26+
private readonly _geocoder: google.maps.Geocoder;
27+
28+
constructor(private readonly _ngZone: NgZone) {
29+
this._geocoder = new google.maps.Geocoder();
30+
}
31+
32+
/**
33+
* See developers.google.com/maps/documentation/javascript/reference/geocoder#Geocoder.geocode
34+
*/
35+
geocode(request: google.maps.GeocoderRequest): Observable<MapGeocoderResponse> {
36+
return new Observable(observer => {
37+
this._geocoder.geocode(request, (results, status) => {
38+
this._ngZone.run(() => {
39+
observer.next({results, status});
40+
observer.complete();
41+
});
42+
});
43+
});
44+
}
45+
}

src/google-maps/public-api.ts

+5-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,10 @@ export {MapBaseLayer} from './map-base-layer';
1313
export {MapBicyclingLayer} from './map-bicycling-layer/map-bicycling-layer';
1414
export {MapCircle} from './map-circle/map-circle';
1515
export {MapDirectionsRenderer} from './map-directions-renderer/map-directions-renderer';
16-
export {MapDirectionsService} from './map-directions-renderer/map-directions-service';
16+
export {
17+
MapDirectionsService,
18+
MapDirectionsResponse,
19+
} from './map-directions-renderer/map-directions-service';
1720
export {MapGroundOverlay} from './map-ground-overlay/map-ground-overlay';
1821
export {MapInfoWindow} from './map-info-window/map-info-window';
1922
export {MapKmlLayer} from './map-kml-layer/map-kml-layer';
@@ -25,3 +28,4 @@ export {MapRectangle} from './map-rectangle/map-rectangle';
2528
export {MapTrafficLayer} from './map-traffic-layer/map-traffic-layer';
2629
export {MapTransitLayer} from './map-transit-layer/map-transit-layer';
2730
export {MapHeatmapLayer, HeatmapData} from './map-heatmap-layer/map-heatmap-layer';
31+
export {MapGeocoder, MapGeocoderResponse} from './map-geocoder/map-geocoder';

src/google-maps/testing/fake-google-map-utils.ts

+23
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ export interface TestingWindow extends Window {
3434
visualization?: {
3535
HeatmapLayer?: jasmine.Spy;
3636
}
37+
Geocoder?: jasmine.Spy;
3738
};
3839
};
3940
MarkerClusterer?: jasmine.Spy;
@@ -562,3 +563,25 @@ export function createLatLngConstructorSpy(
562563
}
563564
return latLngConstructorSpy;
564565
}
566+
567+
/** Creates a jasmine.SpyObj for the Geocoder */
568+
export function createGeocoderSpy(): jasmine.SpyObj<google.maps.Geocoder> {
569+
return jasmine.createSpyObj('google.maps.Geocoder', ['geocode']);
570+
}
571+
572+
/** Creates a jasmine.Spy to watch for the constructor of the Geocoder. */
573+
export function createGeocoderConstructorSpy(
574+
geocoderSpy: jasmine.SpyObj<google.maps.Geocoder>): jasmine.Spy {
575+
const geocoderConstructorSpy = jasmine.createSpy('Geocoder constructor', () => geocoderSpy);
576+
const testingWindow: TestingWindow = window;
577+
if (testingWindow.google && testingWindow.google.maps) {
578+
testingWindow.google.maps['Geocoder'] = geocoderConstructorSpy;
579+
} else {
580+
testingWindow.google = {
581+
maps: {
582+
'Geocoder': geocoderConstructorSpy,
583+
},
584+
};
585+
}
586+
return geocoderConstructorSpy;
587+
}

tools/public_api_guard/google-maps/google-maps.d.ts

+17
Original file line numberDiff line numberDiff line change
@@ -130,13 +130,30 @@ export declare class MapDirectionsRenderer implements OnInit, OnChanges, OnDestr
130130
static ɵfac: i0.ɵɵFactoryDef<MapDirectionsRenderer, never>;
131131
}
132132

133+
export interface MapDirectionsResponse {
134+
result?: google.maps.DirectionsResult;
135+
status: google.maps.DirectionsStatus;
136+
}
137+
133138
export declare class MapDirectionsService {
134139
constructor(_ngZone: NgZone);
135140
route(request: google.maps.DirectionsRequest): Observable<MapDirectionsResponse>;
136141
static ɵfac: i0.ɵɵFactoryDef<MapDirectionsService, never>;
137142
static ɵprov: i0.ɵɵInjectableDef<MapDirectionsService>;
138143
}
139144

145+
export declare class MapGeocoder {
146+
constructor(_ngZone: NgZone);
147+
geocode(request: google.maps.GeocoderRequest): Observable<MapGeocoderResponse>;
148+
static ɵfac: i0.ɵɵFactoryDef<MapGeocoder, never>;
149+
static ɵprov: i0.ɵɵInjectableDef<MapGeocoder>;
150+
}
151+
152+
export interface MapGeocoderResponse {
153+
results: google.maps.GeocoderResult[];
154+
status: google.maps.GeocoderStatus;
155+
}
156+
140157
export declare class MapGroundOverlay implements OnInit, OnDestroy {
141158
get bounds(): google.maps.LatLngBounds | google.maps.LatLngBoundsLiteral;
142159
set bounds(bounds: google.maps.LatLngBounds | google.maps.LatLngBoundsLiteral);

0 commit comments

Comments
 (0)