src/services/google/google-map.service.ts
import { GoogleMarkerClusterer } from '../../models/google/google-marker-clusterer';
import { GoogleInfoWindow } from '../../models/google/google-info-window';
import { Injectable, NgZone } from '@angular/core';
import { Observable, Observer } from 'rxjs';
import { MapService } from '../map.service';
import { MapAPILoader } from '../mapapiloader';
import { GoogleMapAPILoader, GoogleMapAPILoaderConfig } from './google-map-api-loader.service';
import { GoogleClusterService } from './google-cluster.service';
import { ILayerOptions } from '../../interfaces/ilayer-options';
import { IClusterOptions } from '../../interfaces/icluster-options';
import { IMapOptions } from '../../interfaces/imap-options';
import { ILatLong } from '../../interfaces/ilatlong';
import { IPoint } from '../../interfaces/ipoint';
import { ISize } from '../../interfaces/isize';
import { IMarkerOptions } from '../../interfaces/imarker-options';
import { IMarkerIconInfo } from '../../interfaces/imarker-icon-info';
import { IPolygonOptions } from '../../interfaces/ipolygon-options';
import { IPolylineOptions } from '../../interfaces/ipolyline-options';
import { IInfoWindowOptions } from '../../interfaces/iinfo-window-options';
import { MapTypeId } from '../../models/map-type-id';
import { Marker } from '../../models/marker';
import { Polygon } from '../../models/polygon';
import { Polyline } from '../../models/polyline';
import { MixinMapLabelWithOverlayView } from '../../models/google/google-label';
import { MixinCanvasOverlay } from '../../models/google/google-canvas-overlay';
import { GoogleCanvasOverlay } from '../../models/google/google-canvas-overlay';
import { CanvasOverlay } from '../../models/canvas-overlay';
import { Layer } from '../../models/layer';
import { InfoWindow } from '../../models/info-window';
import { GooglePolygon } from '../../models/google/google-polygon';
import { GooglePolyline } from '../../models/google/google-polyline';
import { GoogleConversions } from './google-conversions';
import { GoogleMarker } from '../../models/google/google-marker';
import { GoogleLayer } from '../../models/google/google-layer';
import { IBox } from '../../interfaces/ibox';
import { GoogleMapEventsLookup } from '../../models/google/google-events-lookup';
import * as GoogleMapTypes from './google-map-types';
declare const google: any;
declare const MarkerClusterer: any;
/**
* Concrete implementation of the MapService abstract implementing a Google Maps provider
*
* @export
*/
@Injectable()
export class GoogleMapService implements MapService {
///
/// Field Declarations
///
private _map: Promise<GoogleMapTypes.GoogleMap>;
private _mapInstance: GoogleMapTypes.GoogleMap;
private _mapResolver: (value?: GoogleMapTypes.GoogleMap) => void;
private _config: GoogleMapAPILoaderConfig;
///
/// Property Definitions
///
/**
* Gets the Google Map control instance underlying the implementation
*
* @readonly
* @memberof GoogleMapService
*/
public get MapInstance(): GoogleMapTypes.GoogleMap { return this._mapInstance; }
/**
* Gets a Promise for a Google Map control instance underlying the implementation. Use this instead of {@link MapInstance} if you
* are not sure if and when the instance will be created.
* @readonly
* @memberof GoogleMapService
*/
public get MapPromise(): Promise<GoogleMapTypes.GoogleMap> { return this._map; }
/**
* Gets the maps physical size.
*
* @readonly
* @abstract
* @memberof BingMapService
*/
public get MapSize(): ISize {
if (this.MapInstance) {
const el: HTMLDivElement = this.MapInstance.getDiv();
const s: ISize = { width: el.offsetWidth, height: el.offsetHeight };
return s;
}
return null;
}
///
/// Constructor
///
/**
* Creates an instance of GoogleMapService.
* @param _loader MapAPILoader instance implemented for Google Maps. This instance will generally be injected.
* @param _zone NgZone object to enable zone aware promises. This will generally be injected.
*
* @memberof GoogleMapService
*/
constructor(private _loader: MapAPILoader, private _zone: NgZone) {
this._map = new Promise<GoogleMapTypes.GoogleMap>(
(resolve: (map: GoogleMapTypes.GoogleMap) => void) => { this._mapResolver = resolve; }
);
this._config = (<GoogleMapAPILoader>this._loader).Config;
}
///
/// Public methods and MapService interface implementation
///
/**
* Creates a canvas overlay layer to perform custom drawing over the map with out
* some of the overhead associated with going through the Map objects.
* @param drawCallback A callback function that is triggered when the canvas is ready to be
* rendered for the current map view.
* @returns - Promise of a {@link CanvasOverlay} object.
* @memberof GoogleMapService
*/
public CreateCanvasOverlay(drawCallback: (canvas: HTMLCanvasElement) => void): Promise<CanvasOverlay> {
return this._map.then((map: GoogleMapTypes.GoogleMap) => {
const overlay: GoogleCanvasOverlay = new GoogleCanvasOverlay(drawCallback);
overlay.SetMap(map);
return overlay;
});
}
/*
* Creates a Google map cluster layer within the map context
*
* @param options - Options for the layer. See {@link IClusterOptions}.
* @returns - Promise of a {@link Layer} object, which models the underlying Microsoft.Maps.ClusterLayer object.
*
* @memberof GoogleMapService
*/
public CreateClusterLayer(options: IClusterOptions): Promise<Layer> {
return this._map.then((map: GoogleMapTypes.GoogleMap) => {
let updateOptions: boolean = false;
const markerClusterer: GoogleMapTypes.MarkerClusterer = new MarkerClusterer(map, [], options);
const clusterLayer = new GoogleMarkerClusterer(markerClusterer);
const o: IClusterOptions = {
id: options.id
};
if (!options.visible) {
o.visible = false;
updateOptions = true;
}
if (!options.clusteringEnabled) {
o.clusteringEnabled = false;
updateOptions = true;
}
if (updateOptions) {
clusterLayer.SetOptions(o);
}
return clusterLayer;
});
}
/**
* Creates an information window for a map position
*
* @param [options] - Infowindow options. See {@link IInfoWindowOptions}
* @returns - Promise of a {@link InfoWindow} object, which models the underlying Microsoft.Maps.Infobox object.
*
* @memberof GoogleMapService
*/
public CreateInfoWindow(options?: IInfoWindowOptions): Promise<GoogleInfoWindow> {
return this._map.then((map: GoogleMapTypes.GoogleMap) => {
const o: GoogleMapTypes.InfoWindowOptions = GoogleConversions.TranslateInfoWindowOptions(options);
const infoWindow: GoogleMapTypes.InfoWindow = new google.maps.InfoWindow(o);
return new GoogleInfoWindow(infoWindow, this);
});
}
/**
* Creates a map layer within the map context
*
* @param options - Options for the layer. See {@link ILayerOptions}
* @returns - Promise of a {@link Layer} object, which models the underlying Microsoft.Maps.Layer object.
*
* @memberof GoogleMapService
*/
public CreateLayer(options: ILayerOptions): Promise<Layer> {
return this._map.then((map: GoogleMapTypes.GoogleMap) => {
return new GoogleLayer(map, this, options.id);
});
}
/**
* Creates a map instance
*
* @param el - HTML element to host the map.
* @param mapOptions - Map options
* @returns - Promise fullfilled once the map has been created.
*
* @memberof GoogleMapService
*/
public CreateMap(el: HTMLElement, mapOptions: IMapOptions): Promise<void> {
return this._loader.Load().then(() => {
// apply mixins
MixinMapLabelWithOverlayView();
MixinCanvasOverlay();
// execute map startup
if (!mapOptions.mapTypeId == null) { mapOptions.mapTypeId = MapTypeId.hybrid; }
if (this._mapInstance != null) {
this.DisposeMap();
}
const o: GoogleMapTypes.MapOptions = GoogleConversions.TranslateOptions(mapOptions);
const map: GoogleMapTypes.GoogleMap = new google.maps.Map(el, o);
if (mapOptions.bounds) {
map.fitBounds(GoogleConversions.TranslateBounds(mapOptions.bounds));
}
this._mapInstance = map;
this._mapResolver(map);
return;
});
}
/**
* Creates a Google map marker within the map context
*
* @param [options=<IMarkerOptions>{}] - Options for the marker. See {@link IMarkerOptions}.
* @returns - Promise of a {@link Marker} object, which models the underlying Microsoft.Maps.PushPin object.
*
* @memberof GoogleMapService
*/
public CreateMarker(options: IMarkerOptions = <IMarkerOptions>{}): Promise<Marker> {
const payload = (x: GoogleMapTypes.MarkerOptions, map: GoogleMapTypes.GoogleMap): GoogleMarker => {
const marker = new google.maps.Marker(x);
const m = new GoogleMarker(marker);
m.IsFirst = options.isFirst;
m.IsLast = options.isLast;
if (options.metadata) { options.metadata.forEach((val: any, key: string) => m.Metadata.set(key, val)); }
marker.setMap(map);
return m;
};
return this._map.then((map: GoogleMapTypes.GoogleMap) => {
const o: GoogleMapTypes.MarkerOptions = GoogleConversions.TranslateMarkerOptions(options);
if (options.iconInfo && options.iconInfo.markerType) {
const s = Marker.CreateMarker(options.iconInfo);
if (typeof(s) === 'string') {
o.icon = s;
return payload(o, map);
}
else {
return s.then(x => {
o.icon = x.icon;
return payload(o, map);
});
}
}
else {
return payload(o, map);
}
});
}
/**
* Creates a polygon within the Google Map map context
*
* @abstract
* @param options - Options for the polygon. See {@link IPolygonOptions}.
* @returns - Promise of a {@link Polygon} object, which models the underlying native polygon.
*
* @memberof MapService
*/
public CreatePolygon(options: IPolygonOptions): Promise<Polygon> {
return this._map.then((map: GoogleMapTypes.GoogleMap) => {
const o: GoogleMapTypes.PolygonOptions = GoogleConversions.TranslatePolygonOptions(options);
const polygon: GoogleMapTypes.Polygon = new google.maps.Polygon(o);
polygon.setMap(map);
const p: GooglePolygon = new GooglePolygon(polygon);
if (options.metadata) { options.metadata.forEach((val: any, key: string) => p.Metadata.set(key, val)); }
if (options.title && options.title !== '') { p.Title = options.title; }
if (options.showLabel != null) { p.ShowLabel = options.showLabel; }
if (options.showTooltip != null) { p.ShowTooltip = options.showTooltip; }
if (options.labelMaxZoom != null) { p.LabelMaxZoom = options.labelMaxZoom; }
if (options.labelMinZoom != null) { p.LabelMinZoom = options.labelMinZoom; }
return p;
});
}
/**
* Creates a polyline within the Google Map map context
*
* @abstract
* @param options - Options for the polyline. See {@link IPolylineOptions}.
* @returns - Promise of a {@link Polyline} object (or an array therefore for complex paths)
* which models the underlying native polyline.
*
* @memberof MapService
*/
public CreatePolyline(options: IPolylineOptions): Promise<Polyline|Array<Polyline>> {
let polyline: GoogleMapTypes.Polyline;
return this._map.then((map: GoogleMapTypes.GoogleMap) => {
const o: GoogleMapTypes.PolylineOptions = GoogleConversions.TranslatePolylineOptions(options);
if (options.path && options.path.length > 0 && !Array.isArray(options.path[0])) {
o.path = GoogleConversions.TranslatePaths(options.path)[0];
polyline = new google.maps.Polyline(o);
polyline.setMap(map);
const pl = new GooglePolyline(polyline);
if (options.metadata) { options.metadata.forEach((val: any, key: string) => pl.Metadata.set(key, val)); }
if (options.title && options.title !== '') { pl.Title = options.title; }
if (options.showTooltip != null) { pl.ShowTooltip = options.showTooltip; }
return pl;
}
else {
const paths: Array<Array<GoogleMapTypes.LatLng>> = GoogleConversions.TranslatePaths(options.path);
const lines: Array<Polyline> = new Array<Polyline>();
paths.forEach(p => {
o.path = p;
polyline = new google.maps.Polyline(o);
polyline.setMap(map);
const pl = new GooglePolyline(polyline);
if (options.metadata) { options.metadata.forEach((val: any, key: string) => pl.Metadata.set(key, val)); }
if (options.title && options.title !== '') { pl.Title = options.title; }
if (options.showTooltip != null) { pl.ShowTooltip = options.showTooltip; }
lines.push(pl);
});
return lines;
}
});
}
/**
* Deletes a layer from the map.
*
* @param layer - Layer to delete. See {@link Layer}. This method expects the Google specific Layer model implementation.
* @returns - Promise fullfilled when the layer has been removed.
*
* @memberof GoogleMapService
*/
public DeleteLayer(layer: Layer): Promise<void> {
// return resolved promise as there is no conept of a custom layer in Google.
return Promise.resolve();
}
/**
* Dispaose the map and associated resoures.
*
* @memberof GoogleMapService
*/
public DisposeMap(): void {
if (this._map == null && this._mapInstance == null) { return; }
if (this._mapInstance != null) {
this._mapInstance = null;
this._map = new Promise<GoogleMapTypes.GoogleMap>((resolve: () => void) => { this._mapResolver = resolve; });
}
}
/**
* Gets the geo coordinates of the map center
*
* @returns - A promise that when fullfilled contains the goe location of the center. See {@link ILatLong}.
*
* @memberof GoogleMapService
*/
public GetCenter(): Promise<ILatLong> {
return this._map.then((map: GoogleMapTypes.GoogleMap) => {
const center: GoogleMapTypes.LatLng = map.getCenter();
return <ILatLong>{
latitude: center.lat(),
longitude: center.lng()
};
});
}
/**
* Gets the geo coordinates of the map bounding box
*
* @returns - A promise that when fullfilled contains the geo location of the bounding box. See {@link IBox}.
*
* @memberof GoogleMapService
*/
public GetBounds(): Promise<IBox> {
return this._map.then((map: GoogleMapTypes.GoogleMap) => {
const box = map.getBounds();
return <IBox>{
maxLatitude: box.getNorthEast().lat(),
maxLongitude: Math.max(box.getNorthEast().lng(), box.getSouthWest().lng()),
minLatitude: box.getSouthWest().lat(),
minLongitude: Math.min(box.getNorthEast().lng(), box.getSouthWest().lng()),
center: { latitude: box.getCenter().lat(), longitude: box.getCenter().lng() },
padding: 0
};
});
}
/**
* Gets the current zoom level of the map.
*
* @returns - A promise that when fullfilled contains the zoom level.
*
* @memberof GoogleMapService
*/
public GetZoom(): Promise<number> {
return this._map.then((map: GoogleMapTypes.GoogleMap) => map.getZoom());
}
/**
* Provides a conversion of geo coordinates to pixels on the map control.
*
* @param loc - The geo coordinates to translate.
* @returns - Promise of an {@link IPoint} interface representing the pixels. This promise resolves to null
* if the goe coordinates are not in the view port.
*
* @memberof GoogleMapService
*/
public LocationToPoint(loc: ILatLong): Promise<IPoint> {
return this._map.then((m: GoogleMapTypes.GoogleMap) => {
let crossesDateLine: boolean = false;
const l: GoogleMapTypes.LatLng = GoogleConversions.TranslateLocationObject(loc);
const p = m.getProjection();
const s: number = Math.pow(2, m.getZoom());
const b: GoogleMapTypes.LatLngBounds = m.getBounds();
if (b.getCenter().lng() < b.getSouthWest().lng() ||
b.getCenter().lng() > b.getNorthEast().lng()) { crossesDateLine = true; }
const offsetY: number = p.fromLatLngToPoint(b.getNorthEast()).y;
const offsetX: number = p.fromLatLngToPoint(b.getSouthWest()).x;
const point: GoogleMapTypes.Point = p.fromLatLngToPoint(l);
return {
x: Math.floor((point.x - offsetX + ((crossesDateLine && point.x < offsetX) ? 256 : 0)) * s),
y: Math.floor((point.y - offsetY) * s)
};
});
}
/**
* Provides a conversion of geo coordinates to pixels on the map control.
*
* @param loc - The geo coordinates to translate.
* @returns - Promise of an {@link IPoint} interface array representing the pixels.
*
* @memberof BingMapService
*/
public LocationsToPoints(locs: Array<ILatLong>): Promise<Array<IPoint>> {
return this._map.then((m: GoogleMapTypes.GoogleMap) => {
let crossesDateLine: boolean = false;
const p = m.getProjection();
const s: number = Math.pow(2, m.getZoom());
const b: GoogleMapTypes.LatLngBounds = m.getBounds();
if (b.getCenter().lng() < b.getSouthWest().lng() ||
b.getCenter().lng() > b.getNorthEast().lng()) { crossesDateLine = true; }
const offsetX: number = p.fromLatLngToPoint(b.getSouthWest()).x;
const offsetY: number = p.fromLatLngToPoint(b.getNorthEast()).y;
const l = locs.map(ll => {
const l1: GoogleMapTypes.LatLng = GoogleConversions.TranslateLocationObject(ll);
const point: GoogleMapTypes.Point = p.fromLatLngToPoint(l1);
return {
x: Math.floor((point.x - offsetX + ((crossesDateLine && point.x < offsetX) ? 256 : 0)) * s),
y: Math.floor((point.y - offsetY) * s)
};
});
return l;
});
}
/**
* Centers the map on a geo location.
*
* @param latLng - GeoCoordinates around which to center the map. See {@link ILatLong}
* @returns - Promise that is fullfilled when the center operations has been completed.
*
* @memberof GoogleMapService
*/
public SetCenter(latLng: ILatLong): Promise<void> {
return this._map.then((map: GoogleMapTypes.GoogleMap) => {
const center: GoogleMapTypes.LatLng = GoogleConversions.TranslateLocationObject(latLng);
map.setCenter(center);
});
}
/**
* Sets the generic map options.
*
* @param options - Options to set.
*
* @memberof GoogleMapService
*/
public SetMapOptions(options: IMapOptions) {
this._map.then((m: GoogleMapTypes.GoogleMap) => {
const o: GoogleMapTypes.MapOptions = GoogleConversions.TranslateOptions(options);
m.setOptions(o);
});
}
/**
* Sets the view options of the map.
*
* @param options - Options to set.
*
* @memberof GoogleMapService
*/
public SetViewOptions(options: IMapOptions) {
this._map.then((m: GoogleMapTypes.GoogleMap) => {
if (options.bounds) {
m.fitBounds(GoogleConversions.TranslateBounds(options.bounds));
}
const o: GoogleMapTypes.MapOptions = GoogleConversions.TranslateOptions(options);
m.setOptions(o);
});
}
/**
* Sets the zoom level of the map.
*
* @param zoom - Zoom level to set.
* @returns - A Promise that is fullfilled once the zoom operation is complete.
*
* @memberof GoogleMapService
*/
public SetZoom(zoom: number): Promise<void> {
return this._map.then((map: GoogleMapTypes.GoogleMap) => map.setZoom(zoom));
}
/**
* Creates an event subscription
*
* @param eventName - The name of the event (e.g. 'click')
* @returns - An observable of type E that fires when the event occurs.
*
* @memberof GoogleMapService
*/
public SubscribeToMapEvent<E>(eventName: string): Observable<E> {
const googleEventName: string = GoogleMapEventsLookup[eventName];
return Observable.create((observer: Observer<E>) => {
this._map.then((m: GoogleMapTypes.GoogleMap) => {
m.addListener(googleEventName, (e: any) => {
this._zone.run(() => observer.next(e));
});
});
});
}
/**
* Triggers the given event name on the map instance.
*
* @param eventName - Event to trigger.
* @returns - A promise that is fullfilled once the event is triggered.
*
* @memberof GoogleMapService
*/
public TriggerMapEvent(eventName: string): Promise<void> {
return this._map.then((m) => google.maps.event.trigger(m, eventName, null));
}
}