Skip to content

feat(google-maps): Add MapDirectionsRenderer and MapDirectionsService #21736

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

Merged
merged 3 commits into from
Feb 5, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,4 @@ testem.log
*.log
.ng-dev.user*
.husky/_
/src/dev-app/google-maps-api-key.txt
8 changes: 8 additions & 0 deletions src/dev-app/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,13 @@ create_system_config(
output_name = "system-config.js",
)

# File group for static files that are listed in the gitignore file that contain
# secrets like API keys.
filegroup(
name = "environment-secret-assets",
srcs = glob(["*-api-key.txt"]),
)

# File group for all static files which are needed to serve the dev-app. These files are
# used in the devserver as runfiles and will be copied into the static web package that can
# be deployed on static hosting services (like firebase).
Expand All @@ -129,6 +136,7 @@ filegroup(
srcs = [
"favicon.ico",
"index.html",
":environment-secret-assets",
":system-config",
":theme",
"//src/dev-app/icon:icon_demo_assets",
Expand Down
9 changes: 9 additions & 0 deletions src/dev-app/google-map/google-map-demo.html
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@
<map-traffic-layer *ngIf="isTrafficLayerDisplayed"></map-traffic-layer>
<map-transit-layer *ngIf="isTransitLayerDisplayed"></map-transit-layer>
<map-bicycling-layer *ngIf="isBicyclingLayerDisplayed"></map-bicycling-layer>
<map-directions-renderer *ngIf="directionsResult"
[directions]="directionsResult"></map-directions-renderer>

</google-map>

<p><label>Latitude:</label> {{display?.lat}}</p>
Expand Down Expand Up @@ -150,4 +153,10 @@
</label>
</div>

<div>
<button mat-button (click)="calculateDirections()">
Calculate directions between first two markers
</button>
</div>

</div>
20 changes: 19 additions & 1 deletion src/dev-app/google-map/google-map-demo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import {Component, ViewChild} from '@angular/core';
import {
MapCircle,
MapDirectionsService,
MapInfoWindow,
MapMarker,
MapPolygon,
Expand Down Expand Up @@ -39,7 +40,7 @@ const CIRCLE_RADIUS = 500000;
@Component({
selector: 'google-map-demo',
templateUrl: 'google-map-demo.html',
styleUrls: ['google-map-demo.css']
styleUrls: ['google-map-demo.css'],
})
export class GoogleMapDemo {
@ViewChild(MapInfoWindow) infoWindow: MapInfoWindow;
Expand Down Expand Up @@ -98,6 +99,10 @@ export class GoogleMapDemo {
markerClustererImagePath =
'https://developers.google.com/maps/documentation/javascript/examples/markerclusterer/m';

directionsResult?: google.maps.DirectionsResult;

constructor(private readonly _mapDirectionsService: MapDirectionsService) {}

handleClick(event: google.maps.MapMouseEvent) {
this.markerPositions.push(event.latLng.toJSON());
}
Expand Down Expand Up @@ -190,4 +195,17 @@ export class GoogleMapDemo {
toggleBicyclingLayerDisplay() {
this.isBicyclingLayerDisplayed = !this.isBicyclingLayerDisplayed;
}

calculateDirections() {
if (this.markerPositions.length >= 2) {
const request: google.maps.DirectionsRequest = {
destination: this.markerPositions[1],
origin: this.markerPositions[0],
travelMode: google.maps.TravelMode.DRIVING,
};
this._mapDirectionsService.route(request).subscribe(response => {
this.directionsResult = response.result;
});
}
}
}
20 changes: 19 additions & 1 deletion src/dev-app/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,31 @@
<body>
<dev-app>Loading...</dev-app>

<!-- This iframe loads the hidden Google Maps API Key. -->
<iframe id="google-maps-api-key"
src="google-maps-api-key.txt"
style="display:none;"
onload="loadGoogleMapsScript()"></iframe>

<script src="core-js-bundle/index.js"></script>
<script src="zone.js/dist/zone.js"></script>
<script src="systemjs/dist/system.js"></script>
<script src="system-config.js"></script>
<script src="https://www.youtube.com/iframe_api"></script>
<script src="https://maps.googleapis.com/maps/api/js"></script>
<script src="https://unpkg.com/@googlemaps/markerclustererplus/dist/index.min.js"></script>
<script>
function loadGoogleMapsScript() {
var iframe = document.getElementById('google-maps-api-key');
var googleMapsScript = document.createElement('script');
var googleMapsApiKey = iframe.contentDocument.body.textContent;
var googleMapsUrl = 'https://maps.googleapis.com/maps/api/js';
if (googleMapsApiKey !== 'Page not found') {
googleMapsUrl = googleMapsUrl + '?key=' + googleMapsApiKey;
}
googleMapsScript.src = googleMapsUrl;
document.body.appendChild(googleMapsScript);
}
</script>
<script>
System.config({
map: {
Expand Down
1 change: 1 addition & 0 deletions src/google-maps/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ export class GoogleMapsDemoComponent {
- [`MapTrafficLayer`](./map-traffic-layer/README.md)
- [`MapTransitLayer`](./map-transit-layer/README.md)
- [`MapBicyclingLayer`](./map-bicycling-layer/README.md)
- [`MapDirectionsRenderer`](./map-directions-renderer/README.md)

## The Options Input

Expand Down
2 changes: 2 additions & 0 deletions src/google-maps/google-maps-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {GoogleMap} from './google-map/google-map';
import {MapBaseLayer} from './map-base-layer';
import {MapBicyclingLayer} from './map-bicycling-layer/map-bicycling-layer';
import {MapCircle} from './map-circle/map-circle';
import {MapDirectionsRenderer} from './map-directions-renderer/map-directions-renderer';
import {MapGroundOverlay} from './map-ground-overlay/map-ground-overlay';
import {MapInfoWindow} from './map-info-window/map-info-window';
import {MapKmlLayer} from './map-kml-layer/map-kml-layer';
Expand All @@ -28,6 +29,7 @@ const COMPONENTS = [
MapBaseLayer,
MapBicyclingLayer,
MapCircle,
MapDirectionsRenderer,
MapGroundOverlay,
MapInfoWindow,
MapKmlLayer,
Expand Down
56 changes: 56 additions & 0 deletions src/google-maps/map-directions-renderer/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# MapDirectionsRenderer

The `MapDirectionsRenderer` component wraps the [`google.maps.DirectionsRenderer` class](https://developers.google.com/maps/documentation/javascript/reference/directions#DirectionsRenderer) from the Google Maps JavaScript API. This can easily be used with the `MapDirectionsService` that wraps [`google.maps.DirectionsService`](https://developers.google.com/maps/documentation/javascript/reference/directions#DirectionsService) which is designed to be used with Angular by returning an `Observable` response and works inside the Angular Zone.

The `MapDirectionsService`, like the `google.maps.DirectionsService`, has a single method, `route`. Normally, the `google.maps.DirectionsService` takes two arguments, a `google.maps.DirectionsRequest` and a callback that takes the `google.maps.DirectionsResult` and `google.maps.DirectionsStatus` as arguments. The `MapDirectionsService` route method takes takes the `google.maps.DirectionsRequest` as the single argument, and returns an `Observable` of a `MapDirectionsResponse`, which is an interface defined as follows:

```typescript
export interface MapDirectionsResponse {
status: google.maps.DirectionsStatus;
result?: google.maps.DirectionsResult;
}
```

The most common usecase for the component and class would be to use the `MapDirectionsService` to request a route between two points on the map, and then render them on the map using the `MapDirectionsRenderer`.

## Loading the Library

Using the `MapDirectionsService` requires the Directions API to be enabled in Google Cloud Console on the same project as the one set up for the Google Maps JavaScript API, and requires an API key that has billing enabled. See [here](https://developers.google.com/maps/documentation/javascript/directions#GetStarted) for details.

## Example

```typescript
// google-maps-demo.component.ts
import {Component} from '@angular/core';

@Component({
selector: 'google-map-demo',
templateUrl: 'google-map-demo.html',
})
export class GoogleMapDemo {
center: google.maps.LatLngLiteral = {lat: 24, lng: 12};
zoom = 4;

readonly directionsResults$: Observable<google.maps.DirectionsResult|undefined>;

constructor(mapDirectionsService: MapDirectionsService) {
const request: google.maps.DirectionsRequest = {
destination: {lat: 12, lng: 4},
origin: {lat: 13, lng: 5},
travelMode: google.maps.TravelMode.DRIVING,
};
this.directionsResults$ = mapDirectionsService.route(request).pipe(map(response => response.result));
}
}
```

```html
<!-- google-maps-demo.component.html -->
<google-map height="400px"
width="750px"
[center]="center"
[zoom]="zoom">
<map-directions-renderer *ngIf="(directionsResults$ | async) as directionsResults"
[directions]="directionsResults"></map-directions-renderer>
</google-map>
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import {Component, ViewChild} from '@angular/core';
import {TestBed, waitForAsync} from '@angular/core/testing';
import {By} from '@angular/platform-browser';
import {MapDirectionsRenderer} from './map-directions-renderer';
import {DEFAULT_OPTIONS} from '../google-map/google-map';
import {GoogleMapsModule} from '../google-maps-module';
import {
createDirectionsRendererConstructorSpy,
createDirectionsRendererSpy,
createMapConstructorSpy,
createMapSpy
} from '../testing/fake-google-map-utils';

const DEFAULT_DIRECTIONS: google.maps.DirectionsResult = {
geocoded_waypoints: [],
routes: [],
};

describe('MapDirectionsRenderer', () => {
let mapSpy: jasmine.SpyObj<google.maps.Map>;

beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
imports: [GoogleMapsModule],
declarations: [TestApp],
});
}));

beforeEach(() => {
TestBed.compileComponents();

mapSpy = createMapSpy(DEFAULT_OPTIONS);
createMapConstructorSpy(mapSpy).and.callThrough();
});

afterEach(() => {
(window.google as any) = undefined;
});

it('initializes a Google Maps DirectionsRenderer', () => {
const directionsRendererSpy = createDirectionsRendererSpy({directions: DEFAULT_DIRECTIONS});
const directionsRendererConstructorSpy =
createDirectionsRendererConstructorSpy(directionsRendererSpy).and.callThrough();

const fixture = TestBed.createComponent(TestApp);
fixture.componentInstance.options = {directions: DEFAULT_DIRECTIONS};
fixture.detectChanges();

expect(directionsRendererConstructorSpy)
.toHaveBeenCalledWith({directions: DEFAULT_DIRECTIONS, map: jasmine.any(Object)});
expect(directionsRendererSpy.setMap).toHaveBeenCalledWith(mapSpy);
});

it('sets directions from directions input', () => {
const directionsRendererSpy = createDirectionsRendererSpy({directions: DEFAULT_DIRECTIONS});
const directionsRendererConstructorSpy =
createDirectionsRendererConstructorSpy(directionsRendererSpy).and.callThrough();

const fixture = TestBed.createComponent(TestApp);
fixture.componentInstance.directions = DEFAULT_DIRECTIONS;
fixture.detectChanges();

expect(directionsRendererConstructorSpy)
.toHaveBeenCalledWith({directions: DEFAULT_DIRECTIONS, map: jasmine.any(Object)});
expect(directionsRendererSpy.setMap).toHaveBeenCalledWith(mapSpy);
});

it('gives precedence to directions over options', () => {
const updatedDirections: google.maps.DirectionsResult = {
geocoded_waypoints: [{partial_match: false, place_id: 'test', types: []}],
routes: [],
};
const directionsRendererSpy = createDirectionsRendererSpy({directions: updatedDirections});
const directionsRendererConstructorSpy =
createDirectionsRendererConstructorSpy(directionsRendererSpy).and.callThrough();

const fixture = TestBed.createComponent(TestApp);
fixture.componentInstance.options = {directions: DEFAULT_DIRECTIONS};
fixture.componentInstance.directions = updatedDirections;
fixture.detectChanges();

expect(directionsRendererConstructorSpy)
.toHaveBeenCalledWith({directions: updatedDirections, map: jasmine.any(Object)});
expect(directionsRendererSpy.setMap).toHaveBeenCalledWith(mapSpy);
});

it('exposes methods that provide information from the DirectionsRenderer', () => {
const directionsRendererSpy = createDirectionsRendererSpy({});
createDirectionsRendererConstructorSpy(directionsRendererSpy).and.callThrough();

const fixture = TestBed.createComponent(TestApp);

const directionsRendererComponent =
fixture.debugElement.query(By.directive(MapDirectionsRenderer))!
.injector.get<MapDirectionsRenderer>(MapDirectionsRenderer);
fixture.detectChanges();

directionsRendererSpy.getDirections.and.returnValue(DEFAULT_DIRECTIONS);
expect(directionsRendererComponent.getDirections()).toBe(DEFAULT_DIRECTIONS);

directionsRendererComponent.getPanel();
expect(directionsRendererSpy.getPanel).toHaveBeenCalled();

directionsRendererSpy.getRouteIndex.and.returnValue(10);
expect(directionsRendererComponent.getRouteIndex()).toBe(10);
});

it('initializes DirectionsRenderer event handlers', () => {
const directionsRendererSpy = createDirectionsRendererSpy({});
createDirectionsRendererConstructorSpy(directionsRendererSpy).and.callThrough();

const fixture = TestBed.createComponent(TestApp);
fixture.detectChanges();

expect(directionsRendererSpy.addListener)
.toHaveBeenCalledWith('directions_changed', jasmine.any(Function));
});
});

@Component({
selector: 'test-app',
template: `<google-map>
<map-directions-renderer [options]="options"
[directions]="directions"
(directionsChanged)="handleDirectionsChanged()">
</map-directions-renderer>
</google-map>`,
})
class TestApp {
@ViewChild(MapDirectionsRenderer) directionsRenderer: MapDirectionsRenderer;
options?: google.maps.DirectionsRendererOptions;
directions?: google.maps.DirectionsResult;

handleDirectionsChanged() {}
}
Loading