diff --git a/src/core/directives-const.ts b/src/core/directives-const.ts index 94d052908..b3d4a2750 100644 --- a/src/core/directives-const.ts +++ b/src/core/directives-const.ts @@ -1,6 +1,7 @@ import {SebmGoogleMap} from './directives/google-map'; +import {SebmGoogleMapCircle} from './directives/google-map-circle'; import {SebmGoogleMapInfoWindow} from './directives/google-map-info-window'; import {SebmGoogleMapMarker} from './directives/google-map-marker'; export const GOOGLE_MAPS_DIRECTIVES: any[] = - [SebmGoogleMap, SebmGoogleMapMarker, SebmGoogleMapInfoWindow]; + [SebmGoogleMap, SebmGoogleMapMarker, SebmGoogleMapInfoWindow, SebmGoogleMapCircle]; diff --git a/src/core/directives.ts b/src/core/directives.ts index f7e5d4314..f82998fc4 100644 --- a/src/core/directives.ts +++ b/src/core/directives.ts @@ -1,4 +1,5 @@ export {GOOGLE_MAPS_DIRECTIVES} from './directives-const'; export {SebmGoogleMap} from './directives/google-map'; +export {SebmGoogleMapCircle} from './directives/google-map-circle'; export {SebmGoogleMapInfoWindow} from './directives/google-map-info-window'; export {SebmGoogleMapMarker} from './directives/google-map-marker'; diff --git a/src/core/directives/google-map-circle.ts b/src/core/directives/google-map-circle.ts new file mode 100644 index 000000000..78d9bb070 --- /dev/null +++ b/src/core/directives/google-map-circle.ts @@ -0,0 +1,259 @@ +import {Directive, EventEmitter, OnChanges, OnDestroy, OnInit, SimpleChange} from '@angular/core'; +import {Subscription} from 'rxjs/Subscription'; + +import {MouseEvent} from '../events'; +import {LatLng, LatLngBounds, LatLngLiteral} from '../services/google-maps-types'; +import {MouseEvent as MapMouseEvent} from '../services/google-maps-types'; +import {CircleManager} from '../services/managers/circle-manager'; + +@Directive({ + selector: 'sebm-google-map-circle', + inputs: [ + 'latitude', 'longitude', 'clickable', 'draggable: circleDraggable', 'editable', 'fillColor', + 'fillOpacity', 'radius', 'strokeColor', 'strokeOpacity', 'strokePosition', 'strokeWeight', + 'visible', 'zIndex' + ], + outputs: [ + 'centerChange', 'circleClick', 'circleDblclick', 'drag', 'dragEnd', 'dragStart', 'mouseDown', + 'mouseMove', 'mouseOut', 'mouseOver', 'mouseUp', 'radiusChange', 'rightClick' + ] +}) +export class SebmGoogleMapCircle implements OnInit, + OnChanges, OnDestroy { + /** + * The latitude position of the circle (required). + */ + latitude: number; + + /** + * The clickable position of the circle (required). + */ + longitude: number; + + /** + * Indicates whether this Circle handles mouse events. Defaults to true. + */ + clickable: boolean = true; + + /** + * If set to true, the user can drag this circle over the map. Defaults to false. + */ + draggable: boolean = false; + + /** + * If set to true, the user can edit this circle by dragging the control points shown at + * the center and around the circumference of the circle. Defaults to false. + */ + editable: boolean = false; + + /** + * The fill color. All CSS3 colors are supported except for extended named colors. + */ + fillColor: string; + + /** + * The fill opacity between 0.0 and 1.0. + */ + fillOpacity: number; + + /** + * The radius in meters on the Earth's surface. + */ + radius: number = 0; + + /** + * The stroke color. All CSS3 colors are supported except for extended named colors. + */ + strokeColor: string; + + /** + * The stroke opacity between 0.0 and 1.0 + */ + strokeOpacity: number; + + /** + * The stroke position. Defaults to CENTER. + * This property is not supported on Internet Explorer 8 and earlier. + */ + strokePosition: 'CENTER'|'INSIDE'|'OUTSIDE' = 'CENTER'; + + /** + * The stroke width in pixels. + */ + strokeWeight: number = 0; + + /** + * Whether this circle is visible on the map. Defaults to true. + */ + visible: boolean = true; + + /** + * The zIndex compared to other polys. + */ + zIndex: number; + + /** + * This event is fired when the circle's center is changed. + */ + centerChange: EventEmitter = new EventEmitter(); + + /** + * This event emitter gets emitted when the user clicks on the circle. + */ + circleClick: EventEmitter = new EventEmitter(); + + /** + * This event emitter gets emitted when the user clicks on the circle. + */ + circleDblClick: EventEmitter = new EventEmitter(); + + /** + * This event is repeatedly fired while the user drags the circle. + */ + drag: EventEmitter = new EventEmitter(); + + /** + * This event is fired when the user stops dragging the circle. + */ + dragEnd: EventEmitter = new EventEmitter(); + + /** + * This event is fired when the user starts dragging the circle. + */ + dragStart: EventEmitter = new EventEmitter(); + + /** + * This event is fired when the DOM mousedown event is fired on the circle. + */ + mouseDown: EventEmitter = new EventEmitter(); + + /** + * This event is fired when the DOM mousemove event is fired on the circle. + */ + mouseMove: EventEmitter = new EventEmitter(); + + /** + * This event is fired on circle mouseout. + */ + mouseOut: EventEmitter = new EventEmitter(); + + /** + * This event is fired on circle mouseover. + */ + mouseOver: EventEmitter = new EventEmitter(); + + /** + * This event is fired when the DOM mouseup event is fired on the circle. + */ + mouseUp: EventEmitter = new EventEmitter(); + + /** + * This event is fired when the circle's radius is changed. + */ + radiusChange: EventEmitter = new EventEmitter(); + + /** + * This event is fired when the circle is right-clicked on. + */ + rightClick: EventEmitter = new EventEmitter(); + + private _circleAddedToManager: boolean = false; + + private static _mapOptions: string[] = [ + 'fillColor', 'fillOpacity', 'strokeColor', 'strokeOpacity', 'strokePosition', 'strokeWeight', + 'visible', 'zIndex' + ]; + + private _eventSubscriptions: Subscription[] = []; + + constructor(private _manager: CircleManager) {} + + /** @internal */ + ngOnInit() { + this._manager.addCircle(this); + this._circleAddedToManager = true; + this._registerEventListeners(); + } + + /** @internal */ + ngOnChanges(changes: {[key: string]: SimpleChange}) { + if (!this._circleAddedToManager) { + return; + } + if (changes['latitude'] || changes['longitude']) { + this._manager.setCenter(this); + } + if (changes['editable']) { + this._manager.setEditable(this); + } + if (changes['draggable']) { + this._manager.setDraggable(this); + } + if (changes['visible']) { + this._manager.setVisible(this); + } + if (changes['radius']) { + this._manager.setRadius(this); + } + this._updateCircleOptionsChanges(changes); + } + + private _updateCircleOptionsChanges(changes: {[propName: string]: SimpleChange}) { + let options: {[propName: string]: any} = {}; + let optionKeys = + Object.keys(changes).filter(k => SebmGoogleMapCircle._mapOptions.indexOf(k) !== -1); + optionKeys.forEach((k) => { options[k] = changes[k].currentValue; }); + if (optionKeys.length > 0) { + this._manager.setOptions(this, options); + } + } + + private _registerEventListeners() { + let events: Map> = new Map>(); + events.set('center_changed', this.centerChange); + events.set('click', this.circleClick); + events.set('dblclick', this.circleDblClick); + events.set('drag', this.drag); + events.set('dragend', this.dragEnd); + events.set('dragStart', this.dragStart); + events.set('mousedown', this.mouseDown); + events.set('mousemove', this.mouseMove); + events.set('mouseout', this.mouseOut); + events.set('mouseover', this.mouseOver); + events.set('mouseup', this.mouseUp); + events.set('radius_changed', this.radiusChange); + events.set('rightclick', this.rightClick); + + events.forEach((eventEmitter, eventName) => { + this._eventSubscriptions.push( + this._manager.createEventObservable(eventName, this).subscribe((value) => { + switch (eventName) { + case 'radius_changed': + this._manager.getRadius(this).then((radius) => eventEmitter.emit(radius)); + break; + case 'center_changed': + this._manager.getCenter(this).then( + (center) => + eventEmitter.emit({lat: center.lat(), lng: center.lng()})); + break; + default: + eventEmitter.emit( + {coords: {lat: value.latLng.lat(), lng: value.latLng.lng()}}); + } + })); + }); + } + + /** @internal */ + ngOnDestroy() { + this._eventSubscriptions.forEach(function(s: Subscription) { s.unsubscribe(); }); + this._eventSubscriptions = null; + } + + /** + * Gets the LatLngBounds of this Circle. + */ + getBounds(): Promise { return this._manager.getBounds(this); } + + getCenter(): Promise { return this._manager.getCenter(this); } +} diff --git a/src/core/directives/google-map-info-window.ts b/src/core/directives/google-map-info-window.ts index fd9f98a4f..5b6404c18 100644 --- a/src/core/directives/google-map-info-window.ts +++ b/src/core/directives/google-map-info-window.ts @@ -1,6 +1,6 @@ import {Component, ElementRef, EventEmitter, OnChanges, OnDestroy, SimpleChange} from '@angular/core'; -import {InfoWindowManager} from '../services/info-window-manager'; +import {InfoWindowManager} from '../services/managers/info-window-manager'; import {SebmGoogleMapMarker} from './google-map-marker'; diff --git a/src/core/directives/google-map-marker.ts b/src/core/directives/google-map-marker.ts index 79eb74f92..2ca766356 100644 --- a/src/core/directives/google-map-marker.ts +++ b/src/core/directives/google-map-marker.ts @@ -2,7 +2,7 @@ import {AfterContentInit, ContentChild, Directive, EventEmitter, OnChanges, OnDe import {MouseEvent} from '../events'; import * as mapTypes from '../services/google-maps-types'; -import {MarkerManager} from '../services/marker-manager'; +import {MarkerManager} from '../services/managers/marker-manager'; import {SebmGoogleMapInfoWindow} from './google-map-info-window'; diff --git a/src/core/directives/google-map.ts b/src/core/directives/google-map.ts index 7155d917a..44d49b54a 100644 --- a/src/core/directives/google-map.ts +++ b/src/core/directives/google-map.ts @@ -5,8 +5,9 @@ import {MouseEvent} from '../events'; import {GoogleMapsAPIWrapper} from '../services/google-maps-api-wrapper'; import {LatLng, LatLngLiteral} from '../services/google-maps-types'; import {MapTypeStyle} from '../services/google-maps-types'; -import {InfoWindowManager} from '../services/info-window-manager'; -import {MarkerManager} from '../services/marker-manager'; +import {CircleManager} from '../services/managers/circle-manager'; +import {InfoWindowManager} from '../services/managers/info-window-manager'; +import {MarkerManager} from '../services/managers/marker-manager'; /** * SebMGoogleMap renders a Google Map. @@ -35,7 +36,7 @@ import {MarkerManager} from '../services/marker-manager'; */ @Component({ selector: 'sebm-google-map', - providers: [GoogleMapsAPIWrapper, MarkerManager, InfoWindowManager], + providers: [GoogleMapsAPIWrapper, MarkerManager, InfoWindowManager, CircleManager], inputs: [ 'longitude', 'latitude', 'zoom', 'disableDoubleClickZoom', 'disableDefaultUI', 'scrollwheel', 'backgroundColor', 'draggableCursor', 'draggingCursor', 'keyboardShortcuts', 'zoomControl', diff --git a/src/core/services.ts b/src/core/services.ts index d66172b56..f8da53f8d 100644 --- a/src/core/services.ts +++ b/src/core/services.ts @@ -1,6 +1,7 @@ export {GoogleMapsAPIWrapper} from './services/google-maps-api-wrapper'; -export {InfoWindowManager} from './services/info-window-manager'; +export {CircleManager} from './services/managers/circle-manager'; +export {InfoWindowManager} from './services/managers/info-window-manager'; +export {MarkerManager} from './services/managers/marker-manager'; export {GoogleMapsScriptProtocol, LazyMapsAPILoader, LazyMapsAPILoaderConfig, LazyMapsAPILoaderConfigLiteral, provideLazyMapsAPILoaderConfig} from './services/maps-api-loader/lazy-maps-api-loader'; export {MapsAPILoader} from './services/maps-api-loader/maps-api-loader'; export {NoOpMapsAPILoader} from './services/maps-api-loader/noop-maps-api-loader'; -export {MarkerManager} from './services/marker-manager'; diff --git a/src/core/services/google-maps-api-wrapper.ts b/src/core/services/google-maps-api-wrapper.ts index e79d5ccbe..370c9e5f3 100644 --- a/src/core/services/google-maps-api-wrapper.ts +++ b/src/core/services/google-maps-api-wrapper.ts @@ -49,6 +49,16 @@ export class GoogleMapsAPIWrapper { return this._map.then(() => { return new google.maps.InfoWindow(options); }); } + /** + * Creates a google.map.Circle for the current map. + */ + createCircle(options: mapTypes.CircleOptions): Promise { + return this._map.then((map: mapTypes.GoogleMap) => { + options.map = map; + return new google.maps.Circle(options); + }); + } + subscribeToMapEvent(eventName: string): Observable { return Observable.create((observer: Observer) => { this._map.then((m: mapTypes.GoogleMap) => { diff --git a/src/core/services/google-maps-types.ts b/src/core/services/google-maps-types.ts index 3f7201af5..634aa7a8a 100644 --- a/src/core/services/google-maps-types.ts +++ b/src/core/services/google-maps-types.ts @@ -17,7 +17,7 @@ export interface LatLng { lng(): number; } -export interface Marker { +export interface Marker extends MVCObject { constructor(options?: MarkerOptions): void; setMap(map: GoogleMap): void; setPosition(latLng: LatLng|LatLngLiteral): void; @@ -26,7 +26,6 @@ export interface Marker { setDraggable(draggable: boolean): void; setIcon(icon: string): void; getLabel(): MarkerLabel; - addListener(eventType: string, fn: Function): void; } export interface MarkerOptions { @@ -46,6 +45,63 @@ export interface MarkerLabel { text: string; } +export interface Circle extends MVCObject { + getBounds(): LatLngBounds; + getCenter(): LatLng; + getDraggable(): boolean; + getEditable(): boolean; + getMap(): GoogleMap; + getRadius(): number; + getVisible(): boolean; + setCenter(center: LatLng|LatLngLiteral): void; + setDraggable(draggable: boolean): void; + setEditable(editable: boolean): void; + setMap(map: GoogleMap): void; + setOptions(options: CircleOptions): void; + setRadius(radius: number): void; + setVisible(visible: boolean): void; +} + +export interface CircleOptions { + center?: LatLng|LatLngLiteral; + clickable?: boolean; + draggable?: boolean; + editable?: boolean; + fillColor?: string; + fillOpacity?: number; + map?: GoogleMap; + radius?: number; + strokeColor?: string; + strokeOpacity?: number; + strokePosition?: 'CENTER'|'INSIDE'|'OUTSIDE'; + strokeWeight?: number; + visible?: boolean; + zIndex?: number; +} + +export interface LatLngBounds { + contains(latLng: LatLng): boolean; + equals(other: LatLngBounds|LatLngBoundsLiteral): boolean; + extend(point: LatLng): void; + getCenter(): LatLng; + getNorthEast(): LatLng; + getSouthWest(): LatLng; + intersects(other: LatLngBounds|LatLngBoundsLiteral): boolean; + isEmpty(): boolean; + toJSON(): LatLngBoundsLiteral; + toSpan(): LatLng; + toString(): string; + toUrlValue(precision?: number): string; + union(other: LatLngBounds|LatLngBoundsLiteral): LatLngBounds; +} + +export interface LatLngBoundsLiteral { + east: number; + north: number; + south: number; + west: number; +} + export interface LatLngLiteral { lat: number; lng: number; @@ -108,7 +164,12 @@ export interface InfoWindow { setZIndex(zIndex: number): void; } -export interface MVCObject { constructor(): void; } +export interface MVCObject { + constructor(): void; + addListener(eventName: string, handler: Function): MapsEventListener; +} + +export interface MapsEventListener { remove(): void; } export interface Size { height: number; diff --git a/src/core/services/managers/circle-manager.ts b/src/core/services/managers/circle-manager.ts new file mode 100644 index 000000000..6c95e1463 --- /dev/null +++ b/src/core/services/managers/circle-manager.ts @@ -0,0 +1,86 @@ +import {Injectable, NgZone} from '@angular/core'; + +import {Observable} from 'rxjs/Observable'; +import {Observer} from 'rxjs/Observer'; + +import {SebmGoogleMapCircle} from '../../directives/google-map-circle'; +import {GoogleMapsAPIWrapper} from '../google-maps-api-wrapper'; +import * as mapTypes from '../google-maps-types'; + +@Injectable() +export class CircleManager { + private _circles: Map> = + new Map>(); + + constructor(private _apiWrapper: GoogleMapsAPIWrapper, private _zone: NgZone) {} + + addCircle(circle: SebmGoogleMapCircle) { + this._circles.set(circle, this._apiWrapper.createCircle({ + center: {lat: circle.latitude, lng: circle.longitude}, + clickable: circle.clickable, + draggable: circle.draggable, + editable: circle.editable, + fillColor: circle.fillColor, + fillOpacity: circle.fillOpacity, + radius: circle.radius, + strokeColor: circle.strokeColor, + strokeOpacity: circle.strokeOpacity, + strokePosition: circle.strokePosition, + strokeWeight: circle.strokeWeight, + visible: circle.visible, + zIndex: circle.zIndex + })); + }; + + setOptions(circle: SebmGoogleMapCircle, options: mapTypes.CircleOptions): Promise { + return this._circles.get(circle).then((c) => c.setOptions(options)); + }; + + getBounds(circle: SebmGoogleMapCircle): Promise { + return this._circles.get(circle).then((c) => c.getBounds()); + }; + + getCenter(circle: SebmGoogleMapCircle): Promise { + return this._circles.get(circle).then((c) => c.getCenter()); + }; + + getRadius(circle: SebmGoogleMapCircle): Promise { + return this._circles.get(circle).then((c) => c.getRadius()); + } + + setCenter(circle: SebmGoogleMapCircle): Promise { + return this._circles.get(circle).then( + (c) => { return c.setCenter({lat: circle.latitude, lng: circle.longitude}); }); + }; + + setEditable(circle: SebmGoogleMapCircle): Promise { + return this._circles.get(circle).then((c) => { return c.setEditable(circle.editable); }); + }; + + setDraggable(circle: SebmGoogleMapCircle): Promise { + return this._circles.get(circle).then((c) => { return c.setDraggable(circle.draggable); }); + }; + + setVisible(circle: SebmGoogleMapCircle): Promise { + return this._circles.get(circle).then((c) => { return c.setVisible(circle.visible); }); + }; + + setRadius(circle: SebmGoogleMapCircle): Promise { + return this._circles.get(circle).then((c) => { return c.setRadius(circle.radius); }); + }; + + createEventObservable(eventName: string, circle: SebmGoogleMapCircle): Observable { + return Observable.create((observer: Observer) => { + let listener: mapTypes.MapsEventListener = null; + this._circles.get(circle).then((c) => { + listener = c.addListener(eventName, (e: T) => this._zone.run(() => observer.next(e))); + }); + + return () => { + if (listener !== null) { + listener.remove(); + } + }; + }); + } +} diff --git a/src/core/services/info-window-manager.ts b/src/core/services/managers/info-window-manager.ts similarity index 91% rename from src/core/services/info-window-manager.ts rename to src/core/services/managers/info-window-manager.ts index 915f40710..5c985a3c5 100644 --- a/src/core/services/info-window-manager.ts +++ b/src/core/services/managers/info-window-manager.ts @@ -1,9 +1,9 @@ import {Injectable, NgZone} from '@angular/core'; -import {SebmGoogleMapInfoWindow} from '../directives/google-map-info-window'; +import {SebmGoogleMapInfoWindow} from '../../directives/google-map-info-window'; -import {GoogleMapsAPIWrapper} from './google-maps-api-wrapper'; -import {InfoWindow, InfoWindowOptions} from './google-maps-types'; +import {GoogleMapsAPIWrapper} from '../google-maps-api-wrapper'; +import {InfoWindow, InfoWindowOptions} from '../google-maps-types'; import {MarkerManager} from './marker-manager'; @Injectable() diff --git a/src/core/services/marker-manager.ts b/src/core/services/managers/marker-manager.ts similarity index 92% rename from src/core/services/marker-manager.ts rename to src/core/services/managers/marker-manager.ts index 496bd887b..c7e3ebdd9 100644 --- a/src/core/services/marker-manager.ts +++ b/src/core/services/managers/marker-manager.ts @@ -2,10 +2,10 @@ import {Injectable, NgZone} from '@angular/core'; import {Observable} from 'rxjs/Observable'; import {Observer} from 'rxjs/Observer'; -import {SebmGoogleMapMarker} from '../directives/google-map-marker'; +import {SebmGoogleMapMarker} from '../../directives/google-map-marker'; -import {GoogleMapsAPIWrapper} from './google-maps-api-wrapper'; -import {Marker} from './google-maps-types'; +import {GoogleMapsAPIWrapper} from './../google-maps-api-wrapper'; +import {Marker} from './../google-maps-types'; @Injectable() export class MarkerManager { diff --git a/test/services/marker-manager.spec.ts b/test/services/managers/marker-manager.spec.ts similarity index 91% rename from test/services/marker-manager.spec.ts rename to test/services/managers/marker-manager.spec.ts index de8ac968f..b362b2f72 100644 --- a/test/services/marker-manager.spec.ts +++ b/test/services/managers/marker-manager.spec.ts @@ -1,10 +1,10 @@ import {NgZone, provide} from '@angular/core'; import {async, beforeEachProviders, describe, expect, inject, it} from '@angular/core/testing'; -import {SebmGoogleMapMarker} from '../../src/core/directives/google-map-marker'; -import {GoogleMapsAPIWrapper} from '../../src/core/services/google-maps-api-wrapper'; -import {Marker} from '../../src/core/services/google-maps-types'; -import {MarkerManager} from '../../src/core/services/marker-manager'; +import {SebmGoogleMapMarker} from '../../../src/core/directives/google-map-marker'; +import {GoogleMapsAPIWrapper} from '../../../src/core/services/google-maps-api-wrapper'; +import {Marker} from '../../../src/core/services/google-maps-types'; +import {MarkerManager} from '../../../src/core/services/managers/marker-manager'; export function main() { describe('MarkerManager', () => {