Skip to content

Map overlay #19143

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 2 commits into from
Apr 23, 2020
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
10 changes: 10 additions & 0 deletions src/dev-app/google-map/google-map-demo.html
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@
<map-polygon *ngIf="isPolygonDisplayed" [options]="polygonOptions"></map-polygon>
<map-rectangle *ngIf="isRectangleDisplayed" [options]="rectangleOptions"></map-rectangle>
<map-circle *ngIf="isCircleDisplayed" [options]="circleOptions"></map-circle>
<map-ground-overlay *ngIf="isGroundOverlayDisplayed"
[url]="groundOverlayUrl"
[bounds]="groundOverlayBounds"></map-ground-overlay>
</google-map>

<p><label>Latitude:</label> {{display?.lat}}</p>
Expand Down Expand Up @@ -91,4 +94,11 @@
</label>
</div>

<div>
<label for="ground-overlay-checkbox">
Toggle Ground Overlay
<input type="checkbox" (click)="toggleGroundOverlayDisplay()">
</label>
</div>

</div>
7 changes: 7 additions & 0 deletions src/dev-app/google-map/google-map-demo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,9 @@ export class GoogleMapDemo {
isCircleDisplayed = false;
circleOptions: google.maps.CircleOptions =
{center: CIRCLE_CENTER, radius: CIRCLE_RADIUS, strokeColor: 'grey', strokeOpacity: 0.8};
isGroundOverlayDisplayed = false;
groundOverlayUrl = 'https://angular.io/assets/images/logos/angular/angular.svg';
groundOverlayBounds = RECTANGLE_BOUNDS;

mapTypeId: google.maps.MapTypeId;
mapTypeIds = [
Expand Down Expand Up @@ -142,4 +145,8 @@ export class GoogleMapDemo {
mapTypeChanged(event: Event) {
this.mapTypeId = (event.target as HTMLSelectElement).value as unknown as google.maps.MapTypeId;
}

toggleGroundOverlayDisplay() {
this.isGroundOverlayDisplayed = !this.isGroundOverlayDisplayed;
}
}
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 @@ -10,6 +10,7 @@ import {NgModule} from '@angular/core';

import {GoogleMap} from './google-map/google-map';
import {MapCircle} from './map-circle/map-circle';
import {MapGroundOverlay} from './map-ground-overlay/map-ground-overlay';
import {MapInfoWindow} from './map-info-window/map-info-window';
import {MapMarker} from './map-marker/map-marker';
import {MapPolygon} from './map-polygon/map-polygon';
Expand All @@ -19,6 +20,7 @@ import {MapRectangle} from './map-rectangle/map-rectangle';
const COMPONENTS = [
GoogleMap,
MapCircle,
MapGroundOverlay,
MapInfoWindow,
MapMarker,
MapPolygon,
Expand Down
143 changes: 143 additions & 0 deletions src/google-maps/map-ground-overlay/map-ground-overlay.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
import {Component, ViewChild} from '@angular/core';
import {async, TestBed} from '@angular/core/testing';
import {By} from '@angular/platform-browser';

import {DEFAULT_OPTIONS} from '../google-map/google-map';
import {GoogleMapsModule} from '../google-maps-module';
import {
createGroundOverlayConstructorSpy,
createGroundOverlaySpy,
createMapConstructorSpy,
createMapSpy,
TestingWindow,
} from '../testing/fake-google-map-utils';

import {MapGroundOverlay} from './map-ground-overlay';

describe('MapGroundOverlay', () => {
let mapSpy: jasmine.SpyObj<google.maps.Map>;
const url = 'www.testimg.com/img.jpg';
const bounds: google.maps.LatLngBoundsLiteral = {east: 3, north: 5, west: -3, south: -5};
const clickable = true;
const opacity = 0.5;
const groundOverlayOptions: google.maps.GroundOverlayOptions = {clickable, opacity};

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

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

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

afterEach(() => {
const testingWindow: TestingWindow = window;
delete testingWindow.google;
});

it('initializes a Google Map Ground Overlay', () => {
const groundOverlaySpy = createGroundOverlaySpy(url, bounds, groundOverlayOptions);
const groundOverlayConstructorSpy =
createGroundOverlayConstructorSpy(groundOverlaySpy).and.callThrough();

const fixture = TestBed.createComponent(TestApp);
fixture.componentInstance.url = url;
fixture.componentInstance.bounds = bounds;
fixture.componentInstance.clickable = clickable;
fixture.componentInstance.opacity = opacity;
fixture.detectChanges();

expect(groundOverlayConstructorSpy).toHaveBeenCalledWith(url, bounds, groundOverlayOptions);
expect(groundOverlaySpy.setMap).toHaveBeenCalledWith(mapSpy);
});

it('has an error if required url or bounds are not provided', () => {
expect(() => {
const fixture = TestBed.createComponent(TestApp);
fixture.detectChanges();
}).toThrow(new Error('An image url is required'));
});

it('exposes methods that provide information about the Ground Overlay', () => {
const groundOverlaySpy = createGroundOverlaySpy(url, bounds, groundOverlayOptions);
createGroundOverlayConstructorSpy(groundOverlaySpy).and.callThrough();

const fixture = TestBed.createComponent(TestApp);
const groundOverlayComponent = fixture.debugElement.query(By.directive(
MapGroundOverlay))!.injector.get<MapGroundOverlay>(MapGroundOverlay);
fixture.componentInstance.url = url;
fixture.componentInstance.bounds = bounds;
fixture.componentInstance.opacity = opacity;
fixture.detectChanges();

groundOverlayComponent.getBounds();
expect(groundOverlaySpy.getBounds).toHaveBeenCalled();

groundOverlaySpy.getOpacity.and.returnValue(opacity);
expect(groundOverlayComponent.getOpacity()).toBe(opacity);

groundOverlaySpy.getUrl.and.returnValue(url);
expect(groundOverlayComponent.getUrl()).toBe(url);
});

it('initializes Ground Overlay event handlers', () => {
const groundOverlaySpy = createGroundOverlaySpy(url, bounds, groundOverlayOptions);
createGroundOverlayConstructorSpy(groundOverlaySpy).and.callThrough();

const addSpy = groundOverlaySpy.addListener;
const fixture = TestBed.createComponent(TestApp);
fixture.componentInstance.url = url;
fixture.componentInstance.bounds = bounds;
fixture.detectChanges();

expect(addSpy).toHaveBeenCalledWith('click', jasmine.any(Function));
expect(addSpy).not.toHaveBeenCalledWith('dblclick', jasmine.any(Function));
});

it('should be able to add an event listener after init', () => {
const groundOverlaySpy = createGroundOverlaySpy(url, bounds, groundOverlayOptions);
createGroundOverlayConstructorSpy(groundOverlaySpy).and.callThrough();

const addSpy = groundOverlaySpy.addListener;
const fixture = TestBed.createComponent(TestApp);
fixture.componentInstance.url = url;
fixture.componentInstance.bounds = bounds;
fixture.detectChanges();

expect(addSpy).not.toHaveBeenCalledWith('dblclick', jasmine.any(Function));

// Pick an event that isn't bound in the template.
const subscription = fixture.componentInstance.groundOverlay.mapDblclick.subscribe();
fixture.detectChanges();

expect(addSpy).toHaveBeenCalledWith('dblclick', jasmine.any(Function));
subscription.unsubscribe();
});
});

@Component({
selector: 'test-app',
template: `<google-map>
<map-ground-overlay [url]="url"
[bounds]="bounds"
[clickable]="clickable"
[opacity]="opacity"
(mapClick)="handleClick()">
</map-ground-overlay>
</google-map>`,
})
class TestApp {
@ViewChild(MapGroundOverlay) groundOverlay: MapGroundOverlay;
url!: string;
bounds!: google.maps.LatLngBoundsLiteral;
clickable = false;
opacity = 1;

handleClick() {}
}
165 changes: 165 additions & 0 deletions src/google-maps/map-ground-overlay/map-ground-overlay.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/

// Workaround for: https://github.com/bazelbuild/rules_nodejs/issues/1265
/// <reference types="googlemaps" />

import {Directive, Input, NgZone, OnDestroy, OnInit, Output} from '@angular/core';
import {BehaviorSubject, Observable, Subject} from 'rxjs';
import {map, take, takeUntil} from 'rxjs/operators';

import {GoogleMap} from '../google-map/google-map';
import {MapEventManager} from '../map-event-manager';

/**
* Angular component that renders a Google Maps Ground Overlay via the Google Maps JavaScript API.
*
* See developers.google.com/maps/documentation/javascript/reference/image-overlay#GroundOverlay
*/
@Directive({
selector: 'map-ground-overlay',
})
export class MapGroundOverlay implements OnInit, OnDestroy {
private _eventManager = new MapEventManager(this._ngZone);

private readonly _opacity = new BehaviorSubject<number>(1);
private readonly _destroyed = new Subject<void>();

/**
* The underlying google.maps.GroundOverlay object.
*
* See developers.google.com/maps/documentation/javascript/reference/image-overlay#GroundOverlay
*/
groundOverlay?: google.maps.GroundOverlay;

@Input() url!: string; // Asserted in ngOnInit.

@Input()
bounds!: google.maps.LatLngBounds|google.maps.LatLngBoundsLiteral; // Asserted in ngOnInit.

@Input() clickable = false;

@Input()
set opacity(opacity: number) {
this._opacity.next(opacity);
}

/**
* See
* developers.google.com/maps/documentation/javascript/reference/image-overlay#GroundOverlay.click
*/
@Output()
mapClick: Observable<google.maps.MouseEvent> =
this._eventManager.getLazyEmitter<google.maps.MouseEvent>('click');

/**
* See
* developers.google.com/maps/documentation/javascript/reference/image-overlay
* #GroundOverlay.dblclick
*/
@Output()
mapDblclick: Observable<google.maps.MouseEvent> =
this._eventManager.getLazyEmitter<google.maps.MouseEvent>('dblclick');

constructor(private readonly _map: GoogleMap, private readonly _ngZone: NgZone) {}

ngOnInit() {
if (!this.url) {
throw Error('An image url is required');
}
if (!this.bounds) {
throw Error('Image bounds are required');
}
if (this._map._isBrowser) {
this._combineOptions().pipe(take(1)).subscribe(options => {
// Create the object outside the zone so its events don't trigger change detection.
// We'll bring it back in inside the `MapEventManager` only for the events that the
// user has subscribed to.
this._ngZone.runOutsideAngular(() => {
this.groundOverlay = new google.maps.GroundOverlay(this.url, this.bounds, options);
});
this._assertInitialized();
this.groundOverlay!.setMap(this._map.googleMap!);
this._eventManager.setTarget(this.groundOverlay);
});

this._watchForOpacityChanges();
}
}

ngOnDestroy() {
this._eventManager.destroy();
this._destroyed.next();
this._destroyed.complete();
if (this.groundOverlay) {
this.groundOverlay.setMap(null);
}
}

/**
* See
* developers.google.com/maps/documentation/javascript/reference/image-overlay
* #GroundOverlay.getBounds
*/
getBounds(): google.maps.LatLngBounds {
this._assertInitialized();
return this.groundOverlay!.getBounds();
}

/**
* See
* developers.google.com/maps/documentation/javascript/reference/image-overlay
* #GroundOverlay.getOpacity
*/
getOpacity(): number {
this._assertInitialized();
return this.groundOverlay!.getOpacity();
}

/**
* See
* developers.google.com/maps/documentation/javascript/reference/image-overlay
* #GroundOverlay.getUrl
*/
getUrl(): string {
this._assertInitialized();
return this.groundOverlay!.getUrl();
}

private _combineOptions(): Observable<google.maps.GroundOverlayOptions> {
return this._opacity.pipe(map(opacity => {
const combinedOptions: google.maps.GroundOverlayOptions = {
clickable: this.clickable,
opacity,
};
return combinedOptions;
}));
}

private _watchForOpacityChanges() {
this._opacity.pipe(takeUntil(this._destroyed)).subscribe(opacity => {
if (opacity) {
this._assertInitialized();
this.groundOverlay!.setOpacity(opacity);
}
});
}

private _assertInitialized() {
if (!this._map.googleMap) {
throw Error(
'Cannot access Google Map information before the API has been initialized. ' +
'Please wait for the API to load before trying to interact with it.');
}
if (!this.groundOverlay) {
throw Error(
'Cannot interact with a Google Map GroundOverlay before it has been initialized. ' +
'Please wait for the GroundOverlay to load before trying to interact with it.');
}
}
}
1 change: 1 addition & 0 deletions src/google-maps/public-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
export {GoogleMap} from './google-map/google-map';
export {GoogleMapsModule} from './google-maps-module';
export {MapCircle} from './map-circle/map-circle';
export {MapGroundOverlay} from './map-ground-overlay/map-ground-overlay';
export {MapInfoWindow} from './map-info-window/map-info-window';
export {MapMarker} from './map-marker/map-marker';
export {MapPolygon} from './map-polygon/map-polygon';
Expand Down
Loading