diff --git a/src/core/core-module.ts b/src/core/core-module.ts index 41bfbc433..eb0062d80 100644 --- a/src/core/core-module.ts +++ b/src/core/core-module.ts @@ -1,6 +1,7 @@ import {ModuleWithProviders, NgModule} from '@angular/core'; import {SebmGoogleMapKmlLayer} from './directives/google-map-kml-layer'; +import {SebmGoogleMapDataLayer} from './directives/google-map-data-layer'; import {SebmGoogleMap} from './directives/google-map'; import {SebmGoogleMapCircle} from './directives/google-map-circle'; import {SebmGoogleMapInfoWindow} from './directives/google-map-info-window'; @@ -19,7 +20,8 @@ import {BROWSER_GLOBALS_PROVIDERS} from './utils/browser-globals'; export function coreDirectives() { return [ SebmGoogleMap, SebmGoogleMapMarker, SebmGoogleMapInfoWindow, SebmGoogleMapCircle, - SebmGoogleMapPolygon, SebmGoogleMapPolyline, SebmGoogleMapPolylinePoint, SebmGoogleMapKmlLayer + SebmGoogleMapPolygon, SebmGoogleMapPolyline, SebmGoogleMapPolylinePoint, SebmGoogleMapKmlLayer, + SebmGoogleMapDataLayer ]; }; diff --git a/src/core/directives.ts b/src/core/directives.ts index 682574528..c1de38e37 100644 --- a/src/core/directives.ts +++ b/src/core/directives.ts @@ -2,6 +2,7 @@ export {SebmGoogleMap} from './directives/google-map'; export {SebmGoogleMapCircle} from './directives/google-map-circle'; export {SebmGoogleMapInfoWindow} from './directives/google-map-info-window'; export {SebmGoogleMapKmlLayer} from './directives/google-map-kml-layer'; +export {SebmGoogleMapDataLayer} from './directives/google-map-data-layer'; export {SebmGoogleMapMarker} from './directives/google-map-marker'; export {SebmGoogleMapPolygon} from './directives/google-map-polygon'; export {SebmGoogleMapPolyline} from './directives/google-map-polyline'; diff --git a/src/core/directives/google-map-data-layer.ts b/src/core/directives/google-map-data-layer.ts new file mode 100644 index 000000000..a6d6ac02d --- /dev/null +++ b/src/core/directives/google-map-data-layer.ts @@ -0,0 +1,277 @@ +import { Directive, EventEmitter, OnDestroy, OnInit, OnChanges, SimpleChanges } from '@angular/core'; +import { Subscription } from 'rxjs/Subscription'; + +import { DataMouseEvent, DataOptions } from './../services/google-maps-types'; +import { DataLayerManager } from './../services/managers/data-layer-manager'; + +let layerId = 0; + +/** + * SebmGoogleMapDataLayer enables the user to add data layers to the map. + * + * ### Example + * ```typescript + * import { Component } from 'angular2/core'; + * import { SebmGoogleMap, SebmGoogleMapDataLayer } from + * 'angular2-google-maps/core'; + * + * @Component({ + * selector: 'my-map-cmp', + * directives: [SebmGoogleMap, SebmGoogleMapDataLayer], + * styles: [` + * .sebm-google-map-container { + * height: 300px; + * } + * `], + * template: ` + * + * + * + * + * ` + * }) + * export class MyMapCmp { + * lat: number = -25.274449; + * lng: number = 133.775060; + * zoom: number = 5; + * + * clicked(clickEvent) { + * console.log(clickEvent); + * } + * + * styleFunc(feature) { + * return ({ + * clickable: false, + * fillColor: feature.getProperty('color'), + * strokeWeight: 1 + * }); + * } + * + * geoJsonObject: Object = { + * "type": "FeatureCollection", + * "features": [ + * { + * "type": "Feature", + * "properties": { + * "letter": "G", + * "color": "blue", + * "rank": "7", + * "ascii": "71" + * }, + * "geometry": { + * "type": "Polygon", + * "coordinates": [ + * [ + * [123.61, -22.14], [122.38, -21.73], [121.06, -21.69], [119.66, -22.22], [119.00, -23.40], + * [118.65, -24.76], [118.43, -26.07], [118.78, -27.56], [119.22, -28.57], [120.23, -29.49], + * [121.77, -29.87], [123.57, -29.64], [124.45, -29.03], [124.71, -27.95], [124.80, -26.70], + * [124.80, -25.60], [123.61, -25.64], [122.56, -25.64], [121.72, -25.72], [121.81, -26.62], + * [121.86, -26.98], [122.60, -26.90], [123.57, -27.05], [123.57, -27.68], [123.35, -28.18], + * [122.51, -28.38], [121.77, -28.26], [121.02, -27.91], [120.49, -27.21], [120.14, -26.50], + * [120.10, -25.64], [120.27, -24.52], [120.67, -23.68], [121.72, -23.32], [122.43, -23.48], + * [123.04, -24.04], [124.54, -24.28], [124.58, -23.20], [123.61, -22.14] + * ] + * ] + * } + * }, + * { + * "type": "Feature", + * "properties": { + * "letter": "o", + * "color": "red", + * "rank": "15", + * "ascii": "111" + * }, + * "geometry": { + * "type": "Polygon", + * "coordinates": [ + * [ + * [128.84, -25.76], [128.18, -25.60], [127.96, -25.52], [127.88, -25.52], [127.70, -25.60], + * [127.26, -25.79], [126.60, -26.11], [126.16, -26.78], [126.12, -27.68], [126.21, -28.42], + * [126.69, -29.49], [127.74, -29.80], [128.80, -29.72], [129.41, -29.03], [129.72, -27.95], + * [129.68, -27.21], [129.33, -26.23], [128.84, -25.76] + * ], + * [ + * [128.45, -27.44], [128.32, -26.94], [127.70, -26.82], [127.35, -27.05], [127.17, -27.80], + * [127.57, -28.22], [128.10, -28.42], [128.49, -27.80], [128.45, -27.44] + * ] + * ] + * } + * }, + * { + * "type": "Feature", + * "properties": { + * "letter": "o", + * "color": "yellow", + * "rank": "15", + * "ascii": "111" + * }, + * "geometry": { + * "type": "Polygon", + * "coordinates": [ + * [ + * [131.87, -25.76], [131.35, -26.07], [130.95, -26.78], [130.82, -27.64], [130.86, -28.53], + * [131.26, -29.22], [131.92, -29.76], [132.45, -29.87], [133.06, -29.76], [133.72, -29.34], + * [134.07, -28.80], [134.20, -27.91], [134.07, -27.21], [133.81, -26.31], [133.37, -25.83], + * [132.71, -25.64], [131.87, -25.76] + * ], + * [ + * [133.15, -27.17], [132.71, -26.86], [132.09, -26.90], [131.74, -27.56], [131.79, -28.26], + * [132.36, -28.45], [132.93, -28.34], [133.15, -27.76], [133.15, -27.17] + * ] + * ] + * } + * }, + * { + * "type": "Feature", + * "properties": { + * "letter": "g", + * "color": "blue", + * "rank": "7", + * "ascii": "103" + * }, + * "geometry": { + * "type": "Polygon", + * "coordinates": [ + * [ + * [138.12, -25.04], [136.84, -25.16], [135.96, -25.36], [135.26, -25.99], [135, -26.90], + * [135.04, -27.91], [135.26, -28.88], [136.05, -29.45], [137.02, -29.49], [137.81, -29.49], + * [137.94, -29.99], [137.90, -31.20], [137.85, -32.24], [136.88, -32.69], [136.45, -32.36], + * [136.27, -31.80], [134.95, -31.84], [135.17, -32.99], [135.52, -33.43], [136.14, -33.76], + * [137.06, -33.83], [138.12, -33.65], [138.86, -33.21], [139.30, -32.28], [139.30, -31.24], + * [139.30, -30.14], [139.21, -28.96], [139.17, -28.22], [139.08, -27.41], [139.08, -26.47], + * [138.99, -25.40], [138.73, -25.00], [138.12, -25.04] + * ], + * [ + * [137.50, -26.54], [136.97, -26.47], [136.49, -26.58], [136.31, -27.13], [136.31, -27.72], + * [136.58, -27.99], [137.50, -28.03], [137.68, -27.68], [137.59, -26.78], [137.50, -26.54] + * ] + * ] + * } + * }, + * { + * "type": "Feature", + * "properties": { + * "letter": "l", + * "color": "green", + * "rank": "12", + * "ascii": "108" + * }, + * "geometry": { + * "type": "Polygon", + * "coordinates": [ + * [ + * [140.14, -21.04], [140.31, -29.42], [141.67, -29.49], [141.59, -20.92], [140.14, -21.04] + * ] + * ] + * } + * }, + * { + * "type": "Feature", + * "properties": { + * "letter": "e", + * "color": "red", + * "rank": "5", + * "ascii": "101" + * }, + * "geometry": { + * "type": "Polygon", + * "coordinates": [ + * [ + * [144.14, -27.41], [145.67, -27.52], [146.86, -27.09], [146.82, -25.64], [146.25, -25.04], + * [145.45, -24.68], [144.66, -24.60], [144.09, -24.76], [143.43, -25.08], [142.99, -25.40], + * [142.64, -26.03], [142.64, -27.05], [142.64, -28.26], [143.30, -29.11], [144.18, -29.57], + * [145.41, -29.64], [146.46, -29.19], [146.64, -28.72], [146.82, -28.14], [144.84, -28.42], + * [144.31, -28.26], [144.14, -27.41] + * ], + * [ + * [144.18, -26.39], [144.53, -26.58], [145.19, -26.62], [145.72, -26.35], [145.81, -25.91], + * [145.41, -25.68], [144.97, -25.68], [144.49, -25.64], [144, -25.99], [144.18, -26.39] + * ] + * ] + * } + * } + * ] + * }; + * } + * ``` + */ +@Directive({ + selector: 'sebm-google-map-data-layer', + inputs: ['geoJson', 'style'], + outputs: ['layerClick'] +}) +export class SebmGoogleMapDataLayer implements OnInit, OnDestroy, OnChanges { + private static _dataOptionsAttributes: Array = ['style']; + + private _addedToManager: boolean = false; + private _id: string = (layerId++).toString(); + private _subscriptions: Subscription[] = []; + + /** + * This event is fired when a feature in the layer is clicked. + */ + layerClick: EventEmitter = new EventEmitter(); + + /** + * The geoJson to be displayed + */ + geoJson: Object | null = null; + + /** + * The layer's style function. + */ + style: () => void; + + constructor(private _manager: DataLayerManager) { } + + ngOnInit() { + if (this._addedToManager) { + return; + } + this._manager.addDataLayer(this); + this._addedToManager = true; + this._addEventListeners(); + } + + private _addEventListeners() { + const listeners = [ + { name: 'click', handler: (ev: DataMouseEvent) => this.layerClick.emit(ev) }, + ]; + listeners.forEach((obj) => { + const os = this._manager.createEventObservable(obj.name, this).subscribe(obj.handler); + this._subscriptions.push(os); + }); + } + + /** @internal */ + id(): string { return this._id; } + + /** @internal */ + toString(): string { return `SebmGoogleMapDataLayer-${this._id.toString()}`; } + + /** @internal */ + ngOnDestroy() { + this._manager.deleteDataLayer(this); + // unsubscribe all registered observable subscriptions + this._subscriptions.forEach(s => s.unsubscribe()); + } + + /** @internal */ + ngOnChanges(changes: SimpleChanges) { + if (!this._addedToManager) { + return; + } + + var geoJsonChange = changes['geoJson']; + if (geoJsonChange) { + this._manager.updateGeoJson(this, geoJsonChange.currentValue); + } + + let dataOptions: DataOptions = {}; + const optionKeys = Object.keys(changes).filter( + k => SebmGoogleMapDataLayer._dataOptionsAttributes.indexOf(k) !== -1); + optionKeys.forEach(k => (dataOptions)[k] = changes[k].currentValue); + this._manager.setDataOptions(this, dataOptions); + } +} diff --git a/src/core/directives/google-map.ts b/src/core/directives/google-map.ts index a0101fb1d..652572cbe 100644 --- a/src/core/directives/google-map.ts +++ b/src/core/directives/google-map.ts @@ -10,8 +10,8 @@ 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'; /** * SebMGoogleMap renders a Google Map. @@ -42,7 +42,7 @@ import {KmlLayerManager} from './../services/managers/kml-layer-manager'; selector: 'sebm-google-map', providers: [ GoogleMapsAPIWrapper, MarkerManager, InfoWindowManager, CircleManager, PolylineManager, - PolygonManager, KmlLayerManager + PolygonManager, KmlLayerManager, DataLayerManager ], inputs: [ 'longitude', 'latitude', 'zoom', 'minZoom', 'maxZoom', 'draggable: mapDraggable', diff --git a/src/core/map-types.ts b/src/core/map-types.ts index 9378c853a..bae6883c3 100644 --- a/src/core/map-types.ts +++ b/src/core/map-types.ts @@ -1,7 +1,7 @@ import {LatLngLiteral} from './services/google-maps-types'; // exported map types -export {KmlMouseEvent, LatLngBounds, LatLngBoundsLiteral, LatLngLiteral, PolyMouseEvent} from './services/google-maps-types'; +export {KmlMouseEvent, DataMouseEvent, LatLngBounds, LatLngBoundsLiteral, LatLngLiteral, PolyMouseEvent} from './services/google-maps-types'; /** * MouseEvent gets emitted when the user triggers mouse events on the map. diff --git a/src/core/services.ts b/src/core/services.ts index 66898673f..a57b4240b 100644 --- a/src/core/services.ts +++ b/src/core/services.ts @@ -5,6 +5,7 @@ export {MarkerManager} from './services/managers/marker-manager'; export {PolygonManager} from './services/managers/polygon-manager'; export {PolylineManager} from './services/managers/polyline-manager'; export {KmlLayerManager} from './services/managers/kml-layer-manager'; +export {DataLayerManager} from './services/managers/data-layer-manager'; export {GoogleMapsScriptProtocol, LAZY_MAPS_API_CONFIG, LazyMapsAPILoader, LazyMapsAPILoaderConfigLiteral} 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'; diff --git a/src/core/services/google-maps-types.ts b/src/core/services/google-maps-types.ts index a2e937b4a..dc0da05aa 100644 --- a/src/core/services/google-maps-types.ts +++ b/src/core/services/google-maps-types.ts @@ -1,6 +1,7 @@ export var google: any; export interface GoogleMap extends MVCObject { + data?: Data; constructor(el: HTMLElement, opts?: MapOptions): void; panTo(latLng: LatLng|LatLngLiteral): void; setZoom(zoom: number): void; @@ -356,3 +357,64 @@ export interface KmlMouseEvent extends MouseEvent { featureData: KmlFeatureData; pixelOffset: Size; } + +export interface Data extends MVCObject { + features: Feature[]; + constructor(options?: DataOptions): void; + addGeoJson(geoJson: Object, options?: GeoJsonOptions): Feature[]; + remove(feature: Feature): void; + setControlPosition(controlPosition: ControlPosition): void; + setControls(controls: string[]): void; + setDrawingMode(drawingMode: string): void; + setMap(map: GoogleMap): void; + /* tslint:disable */ + /* + * Tslint configuration check-parameters will prompt errors for these lines of code. + * https://palantir.github.io/tslint/rules/no-unused-variable/ + */ + setStyle(style: () => void): void; + forEach(callback: (feature: Feature) => void): void; + /* tslint:enable */ +} + +export interface Feature extends MVCObject { + id?: number|string|undefined; + geometry: Geometry; + properties: any; +} + +export interface DataOptions{ + controlPosition?: ControlPosition; + controls?: string[]; + drawingMode?: string; + featureFactory?: (geometry: Geometry) => Feature; + map?: GoogleMap; + style?: () => void; +} + +export interface DataMouseEvent extends MouseEvent { + feature: Feature; +} + +export interface GeoJsonOptions { + idPropertyName: string; +} + +export interface Geometry { + type: string; +} + +export class ControlPosition{ + readonly BOTTOM_CENTER: string = 'BOTTOM_CENTER'; + readonly BOTTOM_LEFT: string = 'BOTTOM_LEFT'; + readonly BOTTOM_RIGHT: string = 'BOTTOM_RIGHT'; + readonly LEFT_BOTTOM: string = 'LEFT_BOTTOM'; + readonly LEFT_CENTER: string = 'LEFT_CENTER'; + readonly LEFT_TOP: string = 'LEFT_TOP'; + readonly RIGHT_BOTTOM: string = 'RIGHT_BOTTOM'; + readonly RIGHT_CENTER: string = 'RIGHT_CENTER'; + readonly RIGHT_TOP: string = 'RIGHT_TOP'; + readonly TOP_CENTER: string = 'TOP_CENTER'; + readonly TOP_LEFT: string = 'TOP_LEFT'; + readonly TOP_RIGHT: string = 'TOP_RIGHT'; +} diff --git a/src/core/services/managers/data-layer-manager.ts b/src/core/services/managers/data-layer-manager.ts new file mode 100644 index 000000000..63255dad3 --- /dev/null +++ b/src/core/services/managers/data-layer-manager.ts @@ -0,0 +1,79 @@ +import { Injectable, NgZone } from '@angular/core'; +import { Observable } from 'rxjs/Observable'; +import { Observer } from 'rxjs/Observer'; + +import { SebmGoogleMapDataLayer } from './../../directives/google-map-data-layer'; +import { GoogleMapsAPIWrapper } from './../google-maps-api-wrapper'; +import { Data, DataOptions, Feature } from './../google-maps-types'; + +declare var google: any; + +/** + * Manages all Data Layers for a Google Map instance. + */ +@Injectable() +export class DataLayerManager { + private _layers: Map> = + new Map>(); + + constructor(private _wrapper: GoogleMapsAPIWrapper, private _zone: NgZone) { } + + /** + * Adds a new Data Layer to the map. + */ + addDataLayer(layer: SebmGoogleMapDataLayer) { + const newLayer = this._wrapper.getNativeMap().then(m => { + var dataLayer = new google.maps.Data({ + map: m, + style: layer.style + }); + if (layer.geoJson) { + dataLayer.features = dataLayer.addGeoJson(layer.geoJson); + } + return dataLayer; + }); + this._layers.set(layer, newLayer); + } + + deleteDataLayer(layer: SebmGoogleMapDataLayer) { + this._layers.get(layer).then(l => { + l.setMap(null); + this._layers.delete(layer); + }); + } + + updateGeoJson(layer: SebmGoogleMapDataLayer, geoJson: Object) { + this._layers.get(layer).then(l => { + l.forEach(function (feature: Feature) { + l.remove(feature); + + var index = l.features.indexOf(feature, 0); + if (index > -1) { + l.features.splice(index, 1); + } + }); + l.features = l.addGeoJson(geoJson); + }); + } + + setDataOptions(layer: SebmGoogleMapDataLayer, options: DataOptions) + { + this._layers.get(layer).then(l => { + l.setControlPosition(options.controlPosition); + l.setControls(options.controls); + l.setDrawingMode(options.drawingMode); + l.setStyle(options.style); + }); + } + + /** + * Creates a Google Maps event listener for the given DataLayer as an Observable + */ + createEventObservable(eventName: string, layer: SebmGoogleMapDataLayer): Observable { + return Observable.create((observer: Observer) => { + this._layers.get(layer).then((d: Data) => { + d.addListener(eventName, (e: T) => this._zone.run(() => observer.next(e))); + }); + }); + } +}