diff --git a/src/core/core-module.ts b/src/core/core-module.ts index b28e07ddd..41bfbc433 100644 --- a/src/core/core-module.ts +++ b/src/core/core-module.ts @@ -1,5 +1,6 @@ import {ModuleWithProviders, NgModule} from '@angular/core'; +import {SebmGoogleMapKmlLayer} from './directives/google-map-kml-layer'; import {SebmGoogleMap} from './directives/google-map'; import {SebmGoogleMapCircle} from './directives/google-map-circle'; import {SebmGoogleMapInfoWindow} from './directives/google-map-info-window'; @@ -18,7 +19,7 @@ import {BROWSER_GLOBALS_PROVIDERS} from './utils/browser-globals'; export function coreDirectives() { return [ SebmGoogleMap, SebmGoogleMapMarker, SebmGoogleMapInfoWindow, SebmGoogleMapCircle, - SebmGoogleMapPolygon, SebmGoogleMapPolyline, SebmGoogleMapPolylinePoint + SebmGoogleMapPolygon, SebmGoogleMapPolyline, SebmGoogleMapPolylinePoint, SebmGoogleMapKmlLayer ]; }; diff --git a/src/core/directives.ts b/src/core/directives.ts index 5250e0bd0..682574528 100644 --- a/src/core/directives.ts +++ b/src/core/directives.ts @@ -1,6 +1,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 {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-kml-layer.ts b/src/core/directives/google-map-kml-layer.ts new file mode 100644 index 000000000..c7f9a16a4 --- /dev/null +++ b/src/core/directives/google-map-kml-layer.ts @@ -0,0 +1,126 @@ +import {Directive, EventEmitter, OnChanges, OnDestroy, OnInit, SimpleChanges} from '@angular/core'; +import {Subscription} from 'rxjs/Subscription'; + +import {KmlMouseEvent} from './../services/google-maps-types'; +import {KmlLayerManager} from './../services/managers/kml-layer-manager'; + +let layerId = 0; + +@Directive({ + selector: 'sebm-google-map-kml-layer', + inputs: + ['clickable', 'preserveViewport', 'screenOverlays', 'suppressInfoWindows', 'url', 'zIndex'], + outputs: ['layerClick', 'defaultViewportChange', 'statusChange'] +}) +export class SebmGoogleMapKmlLayer implements OnInit, OnDestroy, OnChanges { + private _addedToManager: boolean = false; + private _id: string = (layerId++).toString(); + private _subscriptions: Subscription[] = []; + private static _kmlLayerOptions: string[] = + ['clickable', 'preserveViewport', 'screenOverlays', 'suppressInfoWindows', 'url', 'zIndex']; + + /** + * If true, the layer receives mouse events. Default value is true. + */ + clickable: boolean = true; + + /** + * By default, the input map is centered and zoomed to the bounding box of the contents of the + * layer. + * If this option is set to true, the viewport is left unchanged, unless the map's center and zoom + * were never set. + */ + preserveViewport: boolean = false; + + /** + * Whether to render the screen overlays. Default true. + */ + screenOverlays: boolean = true; + + /** + * Suppress the rendering of info windows when layer features are clicked. + */ + suppressInfoWindows: boolean = false; + + /** + * The URL of the KML document to display. + */ + url: string = null; + + /** + * The z-index of the layer. + */ + zIndex: number|null = null; + + /** + * This event is fired when a feature in the layer is clicked. + */ + layerClick: EventEmitter = new EventEmitter(); + + /** + * This event is fired when the KML layers default viewport has changed. + */ + defaultViewportChange: EventEmitter = new EventEmitter(); + + /** + * This event is fired when the KML layer has finished loading. + * At this point it is safe to read the status property to determine if the layer loaded + * successfully. + */ + statusChange: EventEmitter = new EventEmitter(); + + constructor(private _manager: KmlLayerManager) {} + + ngOnInit() { + if (this._addedToManager) { + return; + } + this._manager.addKmlLayer(this); + this._addedToManager = true; + this._addEventListeners(); + } + + ngOnChanges(changes: SimpleChanges) { + if (!this._addedToManager) { + return; + } + this._updatePolygonOptions(changes); + } + + private _updatePolygonOptions(changes: SimpleChanges) { + const options = Object.keys(changes) + .filter(k => SebmGoogleMapKmlLayer._kmlLayerOptions.indexOf(k) !== -1) + .reduce((obj: any, k: string) => { + obj[k] = changes[k].currentValue; + return obj; + }, {}); + if (Object.keys(options).length > 0) { + this._manager.setOptions(this, options); + } + } + + private _addEventListeners() { + const listeners = [ + {name: 'click', handler: (ev: KmlMouseEvent) => this.layerClick.emit(ev)}, + {name: 'defaultviewport_changed', handler: () => this.defaultViewportChange.emit()}, + {name: 'status_changed', handler: () => this.statusChange.emit()}, + ]; + 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 `SebmGoogleMapKmlLayer-${this._id.toString()}`; } + + /** @internal */ + ngOnDestroy() { + this._manager.deleteKmlLayer(this); + // unsubscribe all registered observable subscriptions + this._subscriptions.forEach(s => s.unsubscribe()); + } +} diff --git a/src/core/directives/google-map.ts b/src/core/directives/google-map.ts index 21579b5fe..981fb53ac 100644 --- a/src/core/directives/google-map.ts +++ b/src/core/directives/google-map.ts @@ -11,6 +11,8 @@ 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'; + /** * SebMGoogleMap renders a Google Map. * **Important note**: To be able see a map in the browser, you have to define a height for the CSS @@ -40,7 +42,7 @@ import {PolylineManager} from '../services/managers/polyline-manager'; selector: 'sebm-google-map', providers: [ GoogleMapsAPIWrapper, MarkerManager, InfoWindowManager, CircleManager, PolylineManager, - PolygonManager + PolygonManager, KmlLayerManager ], inputs: [ 'longitude', 'latitude', 'zoom', 'draggable: mapDraggable', 'disableDoubleClickZoom', diff --git a/src/core/map-types.ts b/src/core/map-types.ts index ad59c50e6..9378c853a 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 {LatLngBounds, LatLngBoundsLiteral, LatLngLiteral, PolyMouseEvent} from './services/google-maps-types'; +export {KmlMouseEvent, 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 8047df371..66898673f 100644 --- a/src/core/services.ts +++ b/src/core/services.ts @@ -4,6 +4,7 @@ export {InfoWindowManager} from './services/managers/info-window-manager'; 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 {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 e94d8c5e1..a90537679 100644 --- a/src/core/services/google-maps-types.ts +++ b/src/core/services/google-maps-types.ts @@ -293,3 +293,64 @@ export interface Polygon extends MVCObject { setPaths(paths: Array>|Array): void; setVisible(visible: boolean): void; } + +export interface KmlLayer extends MVCObject { + getDefaultViewport(): LatLngBounds; + getMap(): GoogleMap; + getMetadata(): KmlLayerMetadata; + getStatus(): KmlLayerStatus; + getUrl(): string; + getZIndex(): number; + setMap(map: GoogleMap): void; + setOptions(options: KmlLayerOptions): void; + setUrl(url: string): void; + setZIndex(zIndex: number): void; +} + +/** + * See: https://developers.google.com/maps/documentation/javascript/reference?hl=de#KmlLayerStatus + */ +export type KmlLayerStatus = 'DOCUMENT_NOT_FOUND' | + 'DOCUMENT_TOO_LARGE' | 'FETCH_ERROR' | 'INVALID_DOCUMENT' | 'INVALID_REQUEST' | + 'LIMITS_EXCEEDED' | 'OK' | 'TIMED_OUT' | 'UNKNOWN'; + +/** + * See: https://developers.google.com/maps/documentation/javascript/reference?hl=de#KmlLayerMetadata + */ +export interface KmlLayerMetadata { + author: KmlAuthor; + description: string; + hasScreenOverlays: boolean; + name: string; + snippet: string; +} + +export interface KmlAuthor { + email: string; + name: string; + uri: string; +} + +export interface KmlLayerOptions { + clickable?: boolean; + map?: GoogleMap; + preserveViewport?: boolean; + screenOverlays?: boolean; + suppressInfoWindows?: boolean; + url?: string; + zIndex?: number; +} + +export interface KmlFeatureData { + author: KmlAuthor; + description: string; + id: string; + infoWindowHtml: string; + name: string; + snippet: string; +} + +export interface KmlMouseEvent extends MouseEvent { + featureData: KmlFeatureData; + pixelOffset: Size; +} diff --git a/src/core/services/managers/kml-layer-manager.ts b/src/core/services/managers/kml-layer-manager.ts new file mode 100644 index 000000000..c2f89363b --- /dev/null +++ b/src/core/services/managers/kml-layer-manager.ts @@ -0,0 +1,60 @@ +import {Injectable, NgZone} from '@angular/core'; +import {Observable} from 'rxjs/Observable'; +import {Observer} from 'rxjs/Observer'; + +import {SebmGoogleMapKmlLayer} from './../../directives/google-map-kml-layer'; +import {GoogleMapsAPIWrapper} from './../google-maps-api-wrapper'; +import {KmlLayer, KmlLayerOptions} from './../google-maps-types'; + +declare var google: any; + +/** + * Manages all KML Layers for a Google Map instance. + */ +@Injectable() +export class KmlLayerManager { + private _layers: Map> = + new Map>(); + + constructor(private _wrapper: GoogleMapsAPIWrapper, private _zone: NgZone) {} + + /** + * Adds a new KML Layer to the map. + */ + addKmlLayer(layer: SebmGoogleMapKmlLayer) { + const newLayer = this._wrapper.getNativeMap().then(m => { + return new google.maps.KmlLayer({ + clickable: layer.clickable, + map: m, + preserveViewport: layer.preserveViewport, + screenOverlays: layer.screenOverlays, + suppressInfoWindows: layer.suppressInfoWindows, + url: layer.url, + zIndex: layer.zIndex + }); + }); + this._layers.set(layer, newLayer); + } + + setOptions(layer: SebmGoogleMapKmlLayer, options: KmlLayerOptions) { + this._layers.get(layer).then(l => l.setOptions(options)); + } + + deleteKmlLayer(layer: SebmGoogleMapKmlLayer) { + this._layers.get(layer).then(l => { + l.setMap(null); + this._layers.delete(layer); + }); + } + + /** + * Creates a Google Maps event listener for the given KmlLayer as an Observable + */ + createEventObservable(eventName: string, layer: SebmGoogleMapKmlLayer): Observable { + return Observable.create((observer: Observer) => { + this._layers.get(layer).then((m: KmlLayer) => { + m.addListener(eventName, (e: T) => this._zone.run(() => observer.next(e))); + }); + }); + } +}