import { Component, ElementRef, EventEmitter, OnChanges, OnDestroy, OnInit, SimpleChanges, Input, Output } from '@angular/core'; import {Subscription} from 'rxjs/Subscription'; import {MouseEvent} from '../map-types'; import {GoogleMapsAPIWrapper} from '../services/google-maps-api-wrapper'; import {FullscreenControlOptions, LatLng, LatLngLiteral, MapTypeControlOptions, PanControlOptions, RotateControlOptions, ScaleControlOptions, StreetViewControlOptions, ZoomControlOptions} from '../services/google-maps-types'; import {LatLngBounds, LatLngBoundsLiteral, MapTypeStyle} from '../services/google-maps-types'; import {CircleManager} from '../services/managers/circle-manager'; import {InfoWindowManager} from '../services/managers/info-window-manager'; import {MarkerManager} from '../services/managers/marker-manager'; import {PolygonManager} from '../services/managers/polygon-manager'; import {PolylineManager} from '../services/managers/polyline-manager'; import {KmlLayerManager} from './../services/managers/kml-layer-manager'; import {DataLayerManager} from './../services/managers/data-layer-manager'; /** * AgmMap renders a Google Map. * **Important note**: To be able see a map in the browser, you have to define a height for the * element `agm-map`. * * ### Example * ```typescript * import { Component } from '@angular/core'; * * @Component({ * selector: 'my-map-cmp', * styles: [` * agm-map { * height: 300px; * } * `], * template: ` * * * ` * }) * ``` */ @Component({ selector: 'agm-map', providers: [ GoogleMapsAPIWrapper, MarkerManager, InfoWindowManager, CircleManager, PolylineManager, PolygonManager, KmlLayerManager, DataLayerManager ], host: { // todo: deprecated - we will remove it with the next version '[class.sebm-google-map-container]': 'true' }, styles: [` .agm-map-container-inner { width: inherit; height: inherit; } .agm-map-content { display:none; } `], template: `
` }) export class AgmMap implements OnChanges, OnInit, OnDestroy { /** * The longitude that defines the center of the map. */ @Input() longitude: number = 0; /** * The latitude that defines the center of the map. */ @Input() latitude: number = 0; /** * The zoom level of the map. The default zoom level is 8. */ @Input() zoom: number = 8; /** * The minimal zoom level of the map allowed. When not provided, no restrictions to the zoom level * are enforced. */ @Input() minZoom: number; /** * The maximal zoom level of the map allowed. When not provided, no restrictions to the zoom level * are enforced. */ @Input() maxZoom: number; /** * Enables/disables if map is draggable. */ // tslint:disable-next-line:no-input-rename @Input('mapDraggable') draggable: boolean = true; /** * Enables/disables zoom and center on double click. Enabled by default. */ @Input() disableDoubleClickZoom: boolean = false; /** * Enables/disables all default UI of the Google map. Please note: When the map is created, this * value cannot get updated. */ @Input() disableDefaultUI: boolean = false; /** * If false, disables scrollwheel zooming on the map. The scrollwheel is enabled by default. */ @Input() scrollwheel: boolean = true; /** * Color used for the background of the Map div. This color will be visible when tiles have not * yet loaded as the user pans. This option can only be set when the map is initialized. */ @Input() backgroundColor: string; /** * The name or url of the cursor to display when mousing over a draggable map. This property uses * the css * cursor attribute to change the icon. As with the css property, you must specify at * least one fallback cursor that is not a URL. For example: * [draggableCursor]="'url(http://www.example.com/icon.png), auto;'" */ @Input() draggableCursor: string; /** * The name or url of the cursor to display when the map is being dragged. This property uses the * css cursor attribute to change the icon. As with the css property, you must specify at least * one fallback cursor that is not a URL. For example: * [draggingCursor]="'url(http://www.example.com/icon.png), auto;'" */ @Input() draggingCursor: string; /** * If false, prevents the map from being controlled by the keyboard. Keyboard shortcuts are * enabled by default. */ @Input() keyboardShortcuts: boolean = true; /** * The enabled/disabled state of the Zoom control. */ @Input() zoomControl: boolean = true; /** * Options for the Zoom control. */ @Input() zoomControlOptions: ZoomControlOptions; /** * Styles to apply to each of the default map types. Note that for Satellite/Hybrid and Terrain * modes, these styles will only apply to labels and geometry. */ @Input() styles: MapTypeStyle[] = []; /** * When true and the latitude and/or longitude values changes, the Google Maps panTo method is * used to * center the map. See: https://developers.google.com/maps/documentation/javascript/reference#Map */ @Input() usePanning: boolean = false; /** * The initial enabled/disabled state of the Street View Pegman control. * This control is part of the default UI, and should be set to false when displaying a map type * on which the Street View road overlay should not appear (e.g. a non-Earth map type). */ @Input() streetViewControl: boolean = true; /** * Options for the Street View control. */ @Input() streetViewControlOptions: StreetViewControlOptions; /** * Sets the viewport to contain the given bounds. */ @Input() fitBounds: LatLngBoundsLiteral|LatLngBounds = null; /** * The initial enabled/disabled state of the Scale control. This is disabled by default. */ @Input() scaleControl: boolean = false; /** * Options for the scale control. */ @Input() scaleControlOptions: ScaleControlOptions; /** * The initial enabled/disabled state of the Map type control. */ @Input() mapTypeControl: boolean = false; /** * Options for the Map type control. */ @Input() mapTypeControlOptions: MapTypeControlOptions; /** * The initial enabled/disabled state of the Pan control. */ @Input() panControl: boolean = false; /** * Options for the Pan control. */ @Input() panControlOptions: PanControlOptions; /** * The initial enabled/disabled state of the Rotate control. */ @Input() rotateControl: boolean = false; /** * Options for the Rotate control. */ @Input() rotateControlOptions: RotateControlOptions; /** * The initial enabled/disabled state of the Fullscreen control. */ @Input() fullscreenControl: boolean = false; /** * Options for the Fullscreen control. */ @Input() fullscreenControlOptions: FullscreenControlOptions; /** * The map mapTypeId. Defaults to 'roadmap'. */ @Input() mapTypeId: 'roadmap'|'hybrid'|'satellite'|'terrain'|string = 'roadmap'; /** * When false, map icons are not clickable. A map icon represents a point of interest, * also known as a POI. By default map icons are clickable. */ @Input() clickableIcons: boolean = true; /** * This setting controls how gestures on the map are handled. * Allowed values: * - 'cooperative' (Two-finger touch gestures pan and zoom the map. One-finger touch gestures are not handled by the map.) * - 'greedy' (All touch gestures pan or zoom the map.) * - 'none' (The map cannot be panned or zoomed by user gestures.) * - 'auto' [default] (Gesture handling is either cooperative or greedy, depending on whether the page is scrollable or not. */ @Input() gestureHandling: 'cooperative'|'greedy'|'none'|'auto' = 'auto'; /** * Map option attributes that can change over time */ private static _mapOptionsAttributes: string[] = [ 'disableDoubleClickZoom', 'scrollwheel', 'draggable', 'draggableCursor', 'draggingCursor', 'keyboardShortcuts', 'zoomControl', 'zoomControlOptions', 'styles', 'streetViewControl', 'streetViewControlOptions', 'zoom', 'mapTypeControl', 'mapTypeControlOptions', 'minZoom', 'maxZoom', 'panControl', 'panControlOptions', 'rotateControl', 'rotateControlOptions', 'fullscreenControl', 'fullscreenControlOptions', 'scaleControl', 'scaleControlOptions', 'mapTypeId', 'clickableIcons', 'gestureHandling' ]; private _observableSubscriptions: Subscription[] = []; /** * This event emitter gets emitted when the user clicks on the map (but not when they click on a * marker or infoWindow). */ @Output() mapClick: EventEmitter = new EventEmitter(); /** * This event emitter gets emitted when the user right-clicks on the map (but not when they click * on a marker or infoWindow). */ @Output() mapRightClick: EventEmitter = new EventEmitter(); /** * This event emitter gets emitted when the user double-clicks on the map (but not when they click * on a marker or infoWindow). */ @Output() mapDblClick: EventEmitter = new EventEmitter(); /** * This event emitter is fired when the map center changes. */ @Output() centerChange: EventEmitter = new EventEmitter(); /** * This event is fired when the viewport bounds have changed. */ @Output() boundsChange: EventEmitter = new EventEmitter(); /** * This event is fired when the map becomes idle after panning or zooming. */ @Output() idle: EventEmitter = new EventEmitter(); /** * This event is fired when the zoom level has changed. */ @Output() zoomChange: EventEmitter = new EventEmitter(); /** * This event is fired when the google map is fully initialized. * You get the google.maps.Map instance as a result of this EventEmitter. */ @Output() mapReady: EventEmitter = new EventEmitter(); constructor(private _elem: ElementRef, private _mapsWrapper: GoogleMapsAPIWrapper) {} /** @internal */ ngOnInit() { // todo: this should be solved with a new component and a viewChild decorator const container = this._elem.nativeElement.querySelector('.agm-map-container-inner'); this._initMapInstance(container); } private _initMapInstance(el: HTMLElement) { this._mapsWrapper.createMap(el, { center: {lat: this.latitude || 0, lng: this.longitude || 0}, zoom: this.zoom, minZoom: this.minZoom, maxZoom: this.maxZoom, disableDefaultUI: this.disableDefaultUI, disableDoubleClickZoom: this.disableDoubleClickZoom, scrollwheel: this.scrollwheel, backgroundColor: this.backgroundColor, draggable: this.draggable, draggableCursor: this.draggableCursor, draggingCursor: this.draggingCursor, keyboardShortcuts: this.keyboardShortcuts, styles: this.styles, zoomControl: this.zoomControl, zoomControlOptions: this.zoomControlOptions, streetViewControl: this.streetViewControl, streetViewControlOptions: this.streetViewControlOptions, scaleControl: this.scaleControl, scaleControlOptions: this.scaleControlOptions, mapTypeControl: this.mapTypeControl, mapTypeControlOptions: this.mapTypeControlOptions, panControl: this.panControl, panControlOptions: this.panControlOptions, rotateControl: this.rotateControl, rotateControlOptions: this.rotateControlOptions, fullscreenControl: this.fullscreenControl, fullscreenControlOptions: this.fullscreenControlOptions, mapTypeId: this.mapTypeId, clickableIcons: this.clickableIcons, gestureHandling: this.gestureHandling }) .then(() => this._mapsWrapper.getNativeMap()) .then(map => this.mapReady.emit(map)); // register event listeners this._handleMapCenterChange(); this._handleMapZoomChange(); this._handleMapMouseEvents(); this._handleBoundsChange(); this._handleIdleEvent(); } /** @internal */ ngOnDestroy() { // unsubscribe all registered observable subscriptions this._observableSubscriptions.forEach((s) => s.unsubscribe()); } /* @internal */ ngOnChanges(changes: SimpleChanges) { this._updateMapOptionsChanges(changes); this._updatePosition(changes); } private _updateMapOptionsChanges(changes: SimpleChanges) { let options: {[propName: string]: any} = {}; let optionKeys = Object.keys(changes).filter(k => AgmMap._mapOptionsAttributes.indexOf(k) !== -1); optionKeys.forEach((k) => { options[k] = changes[k].currentValue; }); this._mapsWrapper.setMapOptions(options); } /** * Triggers a resize event on the google map instance. * When recenter is true, the of the google map gets called with the current lat/lng values or fitBounds value to recenter the map. * Returns a promise that gets resolved after the event was triggered. */ triggerResize(recenter: boolean = true): Promise { // Note: When we would trigger the resize event and show the map in the same turn (which is a // common case for triggering a resize event), then the resize event would not // work (to show the map), so we trigger the event in a timeout. return new Promise((resolve) => { setTimeout(() => { return this._mapsWrapper.triggerMapEvent('resize').then(() => { if (recenter) { this.fitBounds != null ? this._fitBounds() : this._setCenter(); } resolve(); }); }); }); } private _updatePosition(changes: SimpleChanges) { if (changes['latitude'] == null && changes['longitude'] == null && changes['fitBounds'] == null) { // no position update needed return; } // we prefer fitBounds in changes if (changes['fitBounds'] && this.fitBounds != null) { this._fitBounds(); return; } if (typeof this.latitude !== 'number' || typeof this.longitude !== 'number') { return; } this._setCenter(); } private _setCenter() { let newCenter = { lat: this.latitude, lng: this.longitude, }; if (this.usePanning) { this._mapsWrapper.panTo(newCenter); } else { this._mapsWrapper.setCenter(newCenter); } } private _fitBounds() { if (this.usePanning) { this._mapsWrapper.panToBounds(this.fitBounds); return; } this._mapsWrapper.fitBounds(this.fitBounds); } private _handleMapCenterChange() { const s = this._mapsWrapper.subscribeToMapEvent('center_changed').subscribe(() => { this._mapsWrapper.getCenter().then((center: LatLng) => { this.latitude = center.lat(); this.longitude = center.lng(); this.centerChange.emit({lat: this.latitude, lng: this.longitude}); }); }); this._observableSubscriptions.push(s); } private _handleBoundsChange() { const s = this._mapsWrapper.subscribeToMapEvent('bounds_changed').subscribe(() => { this._mapsWrapper.getBounds().then( (bounds: LatLngBounds) => { this.boundsChange.emit(bounds); }); }); this._observableSubscriptions.push(s); } private _handleMapZoomChange() { const s = this._mapsWrapper.subscribeToMapEvent('zoom_changed').subscribe(() => { this._mapsWrapper.getZoom().then((z: number) => { this.zoom = z; this.zoomChange.emit(z); }); }); this._observableSubscriptions.push(s); } private _handleIdleEvent() { const s = this._mapsWrapper.subscribeToMapEvent('idle').subscribe( () => { this.idle.emit(void 0); }); this._observableSubscriptions.push(s); } private _handleMapMouseEvents() { interface Emitter { emit(value: any): void; } type Event = {name: string, emitter: Emitter}; const events: Event[] = [ {name: 'click', emitter: this.mapClick}, {name: 'rightclick', emitter: this.mapRightClick}, {name: 'dblclick', emitter: this.mapDblClick}, ]; events.forEach((e: Event) => { const s = this._mapsWrapper.subscribeToMapEvent<{latLng: LatLng}>(e.name).subscribe( (event: {latLng: LatLng}) => { const value = {coords: {lat: event.latLng.lat(), lng: event.latLng.lng()}}; e.emitter.emit(value); }); this._observableSubscriptions.push(s); }); } }