-
Notifications
You must be signed in to change notification settings - Fork 6.8k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(google-maps): add heatmap support
Adds support for rendering heatmaps on the `google-map` component using the `map-heatmap-layer` directive. The directive is mostly a direct wrapper around the `google.maps.visualization.HeatmapLayer` class, except for the fact that it also accepts a `LatLngLiteral`, whereas the Google Maps class only accepts `LatLng` objects. I decided to add some logic to convert them automatically, because creating `LatLng` requires the Maps API to have been loaded which can lead to race conditions if it's being loaded lazily.
- Loading branch information
Showing
15 changed files
with
541 additions
and
13 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
# MapHeatmapLayer | ||
|
||
The `MapHeatmapLayer` directive wraps the [`google.maps.visualization.HeatmapLayer` class](https://developers.google.com/maps/documentation/javascript/reference/visualization#HeatmapLayer) from the Google Maps Visualization JavaScript API. It displays | ||
a heatmap layer on the map when it is a content child of a `GoogleMap` component. Like `GoogleMap`, | ||
this directive offers an `options` input as well as a convenience input for passing in the `data` | ||
that is shown on the heatmap. | ||
|
||
## Requirements | ||
|
||
In order to render a heatmap, the Google Maps JavaScript API has to be loaded with the | ||
`visualization` library. To load the library, you have to add `&libraries=visualization` to the | ||
script that loads the Google Maps API. E.g. | ||
|
||
**Before:** | ||
```html | ||
<script src="https://maps.googleapis.com/maps/api/js?key=YOUR_API_KEY"></script> | ||
``` | ||
|
||
**After:** | ||
```html | ||
<script src="https://maps.googleapis.com/maps/api/js?key=YOUR_API_KEY&libraries=visualization"></script> | ||
``` | ||
|
||
More information: https://developers.google.com/maps/documentation/javascript/heatmaplayer | ||
|
||
## Example | ||
|
||
```typescript | ||
// google-map-demo.component.ts | ||
import {Component} from '@angular/core'; | ||
|
||
@Component({ | ||
selector: 'google-map-demo', | ||
templateUrl: 'google-map-demo.html', | ||
}) | ||
export class GoogleMapDemo { | ||
center = {lat: 37.774546, lng: -122.433523}; | ||
zoom = 12; | ||
heatmapOptions = {radius: 5}; | ||
heatmapData = [ | ||
{lat: 37.782, lng: -122.447}, | ||
{lat: 37.782, lng: -122.445}, | ||
{lat: 37.782, lng: -122.443}, | ||
{lat: 37.782, lng: -122.441}, | ||
{lat: 37.782, lng: -122.439}, | ||
{lat: 37.782, lng: -122.437}, | ||
{lat: 37.782, lng: -122.435}, | ||
{lat: 37.785, lng: -122.447}, | ||
{lat: 37.785, lng: -122.445}, | ||
{lat: 37.785, lng: -122.443}, | ||
{lat: 37.785, lng: -122.441}, | ||
{lat: 37.785, lng: -122.439}, | ||
{lat: 37.785, lng: -122.437}, | ||
{lat: 37.785, lng: -122.435} | ||
]; | ||
} | ||
``` | ||
|
||
```html | ||
<!-- google-map-demo.component.html --> | ||
<google-map height="400px" width="750px" [center]="center" [zoom]="zoom"> | ||
<map-heatmap-layer [data]="heatmapData" [options]="heatmapOptions"></map-heatmap-layer> | ||
</google-map> | ||
``` |
166 changes: 166 additions & 0 deletions
166
src/google-maps/map-heatmap-layer/map-heatmap-layer.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,166 @@ | ||
import {Component, ViewChild} from '@angular/core'; | ||
import {waitForAsync, TestBed} from '@angular/core/testing'; | ||
|
||
import {DEFAULT_OPTIONS} from '../google-map/google-map'; | ||
|
||
import {GoogleMapsModule} from '../google-maps-module'; | ||
import { | ||
createMapConstructorSpy, | ||
createMapSpy, | ||
createHeatmapLayerConstructorSpy, | ||
createHeatmapLayerSpy, | ||
createLatLngSpy, | ||
createLatLngConstructorSpy | ||
} from '../testing/fake-google-map-utils'; | ||
import {HeatmapData, MapHeatmapLayer} from './map-heatmap-layer'; | ||
|
||
describe('MapHeatmapLayer', () => { | ||
let mapSpy: jasmine.SpyObj<google.maps.Map>; | ||
let latLngSpy: jasmine.SpyObj<google.maps.LatLng>; | ||
|
||
beforeEach(waitForAsync(() => { | ||
TestBed.configureTestingModule({ | ||
imports: [GoogleMapsModule], | ||
declarations: [TestApp], | ||
}); | ||
})); | ||
|
||
beforeEach(() => { | ||
TestBed.compileComponents(); | ||
mapSpy = createMapSpy(DEFAULT_OPTIONS); | ||
latLngSpy = createLatLngSpy(); | ||
createMapConstructorSpy(mapSpy).and.callThrough(); | ||
createLatLngConstructorSpy(latLngSpy).and.callThrough(); | ||
}); | ||
|
||
afterEach(() => { | ||
(window.google as any) = undefined; | ||
}); | ||
|
||
it('initializes a Google Map heatmap layer', () => { | ||
const heatmapSpy = createHeatmapLayerSpy(); | ||
const heatmapConstructorSpy = createHeatmapLayerConstructorSpy(heatmapSpy).and.callThrough(); | ||
|
||
const fixture = TestBed.createComponent(TestApp); | ||
fixture.detectChanges(); | ||
|
||
expect(heatmapConstructorSpy).toHaveBeenCalledWith({ | ||
data: [], | ||
map: mapSpy, | ||
}); | ||
}); | ||
|
||
it('should throw if the `visualization` library has not been loaded', () => { | ||
createHeatmapLayerConstructorSpy(createHeatmapLayerSpy()); | ||
delete (window.google.maps as any).visualization; | ||
|
||
expect(() => { | ||
const fixture = TestBed.createComponent(TestApp); | ||
fixture.detectChanges(); | ||
}).toThrowError(/Namespace `google.maps.visualization` not found, cannot construct heatmap/); | ||
}); | ||
|
||
it('sets heatmap inputs', () => { | ||
const options: google.maps.visualization.HeatmapLayerOptions = { | ||
map: mapSpy, | ||
data: [ | ||
new google.maps.LatLng(37.782, -122.447), | ||
new google.maps.LatLng(37.782, -122.445), | ||
new google.maps.LatLng(37.782, -122.443) | ||
] | ||
}; | ||
const heatmapSpy = createHeatmapLayerSpy(); | ||
const heatmapConstructorSpy = createHeatmapLayerConstructorSpy(heatmapSpy).and.callThrough(); | ||
|
||
const fixture = TestBed.createComponent(TestApp); | ||
fixture.componentInstance.data = options.data; | ||
fixture.detectChanges(); | ||
|
||
expect(heatmapConstructorSpy).toHaveBeenCalledWith(options); | ||
}); | ||
|
||
it('sets heatmap options, ignoring map', () => { | ||
const options: Partial<google.maps.visualization.HeatmapLayerOptions> = { | ||
radius: 5, | ||
dissipating: true | ||
}; | ||
const data = [ | ||
new google.maps.LatLng(37.782, -122.447), | ||
new google.maps.LatLng(37.782, -122.445), | ||
new google.maps.LatLng(37.782, -122.443) | ||
]; | ||
const heatmapSpy = createHeatmapLayerSpy(); | ||
const heatmapConstructorSpy = createHeatmapLayerConstructorSpy(heatmapSpy).and.callThrough(); | ||
|
||
const fixture = TestBed.createComponent(TestApp); | ||
fixture.componentInstance.data = data; | ||
fixture.componentInstance.options = options; | ||
fixture.detectChanges(); | ||
|
||
expect(heatmapConstructorSpy).toHaveBeenCalledWith({...options, map: mapSpy, data}); | ||
}); | ||
|
||
it('exposes methods that provide information about the heatmap', () => { | ||
const heatmapSpy = createHeatmapLayerSpy(); | ||
createHeatmapLayerConstructorSpy(heatmapSpy).and.callThrough(); | ||
|
||
const fixture = TestBed.createComponent(TestApp); | ||
fixture.detectChanges(); | ||
const heatmap = fixture.componentInstance.heatmap; | ||
|
||
heatmapSpy.getData.and.returnValue([] as any); | ||
expect(heatmap.getData()).toEqual([]); | ||
}); | ||
|
||
it('should update the heatmap data when the input changes', () => { | ||
const heatmapSpy = createHeatmapLayerSpy(); | ||
const heatmapConstructorSpy = createHeatmapLayerConstructorSpy(heatmapSpy).and.callThrough(); | ||
let data = [ | ||
new google.maps.LatLng(1, 2), | ||
new google.maps.LatLng(3, 4), | ||
new google.maps.LatLng(5, 6) | ||
]; | ||
|
||
const fixture = TestBed.createComponent(TestApp); | ||
fixture.componentInstance.data = data; | ||
fixture.detectChanges(); | ||
|
||
expect(heatmapConstructorSpy).toHaveBeenCalledWith(jasmine.objectContaining({data})); | ||
data = [ | ||
new google.maps.LatLng(7, 8), | ||
new google.maps.LatLng(9, 10), | ||
new google.maps.LatLng(11, 12) | ||
]; | ||
fixture.componentInstance.data = data; | ||
fixture.detectChanges(); | ||
|
||
expect(heatmapSpy.setData).toHaveBeenCalledWith(data); | ||
}); | ||
|
||
it('should create a LatLng object if a LatLngLiteral is passed in', () => { | ||
const latLngConstructor = createLatLngConstructorSpy(latLngSpy).and.callThrough(); | ||
createHeatmapLayerConstructorSpy(createHeatmapLayerSpy()).and.callThrough(); | ||
const fixture = TestBed.createComponent(TestApp); | ||
fixture.componentInstance.data = [{lat: 1, lng: 2}, {lat: 3, lng: 4}]; | ||
fixture.detectChanges(); | ||
|
||
expect(latLngConstructor).toHaveBeenCalledWith(1, 2); | ||
expect(latLngConstructor).toHaveBeenCalledWith(3, 4); | ||
expect(latLngConstructor).toHaveBeenCalledTimes(2); | ||
}); | ||
|
||
}); | ||
|
||
@Component({ | ||
selector: 'test-app', | ||
template: ` | ||
<google-map> | ||
<map-heatmap-layer [data]="data" [options]="options"> | ||
</map-heatmap-layer> | ||
</google-map>`, | ||
}) | ||
class TestApp { | ||
@ViewChild(MapHeatmapLayer) heatmap: MapHeatmapLayer; | ||
options?: Partial<google.maps.visualization.HeatmapLayerOptions>; | ||
data?: HeatmapData; | ||
} |
Oops, something went wrong.