Skip to content

Commit

Permalink
feat(GoogleMapsAPILoading): lazy load mechanism
Browse files Browse the repository at this point in the history
With this optional mechanism, angular2-google-maps will load
the Google Maps API script when a <sebm-google-map>
component is in use for the first time.

BREAKING CHANGE:

You have to add the ANGULAR2_GOOGLE_MAPS_PROVIDERS to your bootstrap() method.

Closes #4
  • Loading branch information
sebholstein committed Nov 14, 2015
1 parent 610362e commit d05e6d3
Show file tree
Hide file tree
Showing 11 changed files with 199 additions and 70 deletions.
10 changes: 10 additions & 0 deletions src/angular2_google_maps.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,12 @@
import {Provider} from 'angular2/angular2';

import {MapsAPILoader} from './services/maps_api_loader/maps_api_loader';
import {LazyMapsAPILoader} from './services/maps_api_loader/lazy_maps_api_loader';

// main module
export * from './components';
export * from './services';

export const ANGULAR2_GOOGLE_MAPS_PROVIDERS: any[] = [
new Provider(MapsAPILoader, {useClass: LazyMapsAPILoader}),
];
4 changes: 2 additions & 2 deletions src/components.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
export * from './components/google_map';
export * from './components/google_map_marker';
export {SebmGoogleMap} from './components/google_map';
export {SebmGoogleMapMarker} from './components/google_map_marker';
24 changes: 11 additions & 13 deletions src/components/google_map.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,26 +13,22 @@ import {
QueryList,
provide
} from 'angular2/angular2';
import {
GoogleMapsAPIWrapper,
GoogleMapsAPIWrapperFactory
} from '../services/google_maps_api_wrapper';
import {GoogleMapsAPIWrapper} from '../services/google_maps_api_wrapper';
import {SebmGoogleMapMarker} from './google_map_marker';
import {MarkerManager} from '../services/marker_manager';
import {MapsAPILoader} from '../services/maps_api_loader/maps_api_loader';

/**
* Todo: add docs
*/
@Component({
selector: 'sebm-google-map',
providers: [GoogleMapsAPIWrapperFactory, MarkerManager],
viewProviders: [MarkerManager],
providers: [GoogleMapsAPIWrapper, MarkerManager],
styles: [
`
.sebm-google-map-container-inner {
width: inherit;
height: inherit;
display: block;
}
`
],
Expand All @@ -46,17 +42,19 @@ export class SebmGoogleMap {
private _latitude: number = 0;
private _zoom: number = 8;
private _mapsWrapper: GoogleMapsAPIWrapper;
private _zone: NgZone;

constructor(
private elem: ElementRef, private _zone: NgZone, mapsFactory: GoogleMapsAPIWrapperFactory,
renderer: Renderer) {
elem: ElementRef, _mapsWrapper: GoogleMapsAPIWrapper, _zone: NgZone, renderer: Renderer) {
this._mapsWrapper = _mapsWrapper;
this._zone = _zone;
renderer.setElementClass(elem, 'sebm-google-map-container', true);
this._initMapInstance(
elem.nativeElement.querySelector('.sebm-google-map-container-inner'), mapsFactory);
const container = elem.nativeElement.querySelector('.sebm-google-map-container-inner');
this._initMapInstance(container);
}

private _initMapInstance(el: HTMLElement, mapsFactory: GoogleMapsAPIWrapperFactory) {
this._mapsWrapper = mapsFactory.create(el, this._latitude, this._longitude);
private _initMapInstance(el: HTMLElement) {
this._mapsWrapper.createMap(el, this._latitude, this._longitude);
this._handleMapsCenterChanged();
this._handleZoomChanged();
}
Expand Down
8 changes: 2 additions & 6 deletions src/components/google_map_marker.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import {Directive, Input, provide, Host, Inject, SkipSelf, SimpleChange} from 'angular2/angular2';
import {Directive, Input, SimpleChange} from 'angular2/angular2';
import {GoogleMapsAPIWrapper} from '../services/google_maps_api_wrapper';
import {MarkerManager} from '../services/marker_manager';
import {SebmGoogleMap} from './google_map';

let markerId = 0;

Expand All @@ -14,10 +13,7 @@ export class SebmGoogleMapMarker {
private _markerAddedToManger: boolean = false;
private _id: string;

constructor(
@Host() @SkipSelf() private _map: SebmGoogleMap, private _markerManager: MarkerManager) {
this._id = (markerId++).toString();
}
constructor(private _markerManager: MarkerManager) { this._id = (markerId++).toString(); }

onChanges(changes: {[key: string]: SimpleChange}) {
if (!this._markerAddedToManger && this.latitude && this.longitude) {
Expand Down
9 changes: 9 additions & 0 deletions src/custom_typings/google_maps.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ declare module google.maps {
setMap(map: Map): void;
setPosition(latLng: LatLng | LatLngLiteral): void;
setTitle(title: string): void;
setLabel(label: string | MarkerLabel): void;
}

export interface MarkerOptions {
Expand All @@ -28,6 +29,14 @@ declare module google.maps {
map?: Map;
}

export interface MarkerLabel {
color: string;
fontFamily: string;
fontSize: string;
fontWeight: string;
text: string;
}

export interface LatLngLiteral {
lat: number;
lng: number;
Expand Down
7 changes: 7 additions & 0 deletions src/services.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export {MapsAPILoader} from './services/maps_api_loader/maps_api_loader';
export {NoOpMapsAPILoader} from './services/maps_api_loader/noop_maps_api_loader';
export {
LazyMapsAPILoader,
LazyMapsAPILoaderConfig,
GoogleMapsScriptProtocol
} from './services/maps_api_loader/lazy_maps_api_loader';
74 changes: 39 additions & 35 deletions src/services/google_maps_api_wrapper.ts
Original file line number Diff line number Diff line change
@@ -1,49 +1,67 @@
import {Injectable, Inject, NgZone, ElementRef} from 'angular2/angular2';
import {Observable} from 'rx';

import {MapsAPILoader} from './maps_api_loader/maps_api_loader';

/**
* Wrapper class that handles the communication with the Google Maps Javascript
* API v3
*/
@Injectable()
export class GoogleMapsAPIWrapper {
private _el: HTMLElement;
private _map: google.maps.Map;
private _map: Promise<google.maps.Map>;

private _centerChangeObservable: Observable<google.maps.LatLngLiteral>;
private _zoomChangeObservable: Observable<number>;

constructor(_el: HTMLElement, latitude: number, longitude: number, private _zone: NgZone) {
this._el = _el;
this._map = new google.maps.Map(this._el, {center: {lat: latitude, lng: longitude}});
private _mapResolver: (value?: google.maps.Map) => void;

constructor(private _zone: NgZone, private _loader: MapsAPILoader) {
this._createObservables();
this._map =
new Promise<google.maps.Map>((resolve: () => void) => { this._mapResolver = resolve; });
}

createMap(el: HTMLElement, latitude: number, longitude: number): Promise<void> {
return this._loader.load().then(() => {
const map = new google.maps.Map(el, {center: {lat: latitude, lng: longitude}});
this._mapResolver(map);
return;
});
}

createEventObservable<E>(eventName: string, callback: (observer: Rx.Observer<E>) => void):
Observable<E> {
return Observable.create((observer: Rx.Observer<E>) => {
this._map.addListener(eventName, () => { callback(observer); });
this._map.then(
(m: google.maps.Map) => m.addListener(eventName, () => { callback(observer); }));
});
}

private _createObservables() {
this._centerChangeObservable = this.createEventObservable<google.maps.LatLngLiteral>(
'center_changed', (observer: Rx.Observer<google.maps.LatLngLiteral>) => {
const center = this._map.getCenter();
observer.onNext({lat: center.lat(), lng: center.lng()});
this._map.then((map: google.maps.Map) => {
const center = map.getCenter();
observer.onNext({lat: center.lat(), lng: center.lng()});
});
});
this._zoomChangeObservable =
this.createEventObservable<number>('zoom_changed', (observer: Rx.Observer<number>) => {
this._map.then((map: google.maps.Map) => { observer.onNext(map.getZoom()); });
});
this._zoomChangeObservable = this.createEventObservable<number>(
'zoom_changed',
(observer: Rx.Observer<number>) => { observer.onNext(this._map.getZoom()); });
}

/**
* Creates a google map marker with the map context
*/
createMarker(options: google.maps.MarkerOptions = <google.maps.MarkerOptions>{}):
google.maps.Marker {
options.map = this._map;
return new google.maps.Marker(options);
Promise<google.maps.Marker> {
return this._map.then((map: google.maps.Map) => {
options.map = map;
return new google.maps.Marker(options);
});
}

getZoomChangeObserable(): Observable<number> { return this._zoomChangeObservable; }
Expand All @@ -52,29 +70,15 @@ export class GoogleMapsAPIWrapper {
return this._centerChangeObservable;
}

setCenter(latLng: google.maps.LatLngLiteral) { this._map.setCenter(latLng); }

setZoom(zoom: number) { this._map.setZoom(zoom); }

getCenter(): google.maps.LatLng { return this._map.getCenter(); }
}

// todo: change name, because it's not a real factory.
// We have to create the instance with the component element and I don't see
// any chances to modify the viewproviders for <sebm-google-map> after the
// component instance is created.
@Injectable()
export class GoogleMapsAPIWrapperFactory {
private _instance: GoogleMapsAPIWrapper;

constructor(private _zone: NgZone) {}
setCenter(latLng: google.maps.LatLngLiteral): Promise<void> {
return this._map.then((map: google.maps.Map) => map.setCenter(latLng));
}

create(el: HTMLElement, latitude: number, longitude: number): GoogleMapsAPIWrapper {
if (this._instance) {
throw new Error('instance already created');
}
return this._instance = new GoogleMapsAPIWrapper(el, latitude, latitude, this._zone);
setZoom(zoom: number): Promise<void> {
return this._map.then((map: google.maps.Map) => map.setZoom(zoom));
}

getInstance(): GoogleMapsAPIWrapper { return this._instance; }
getCenter(): Promise<google.maps.LatLng> {
return this._map.then((map: google.maps.Map) => map.getCenter());
}
}
81 changes: 81 additions & 0 deletions src/services/maps_api_loader/lazy_maps_api_loader.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import {Injectable, forwardRef, Optional} from 'angular2/angular2';
import {MapsAPILoader} from './maps_api_loader';

export enum GoogleMapsScriptProtocol {
HTTP,
HTTPS,
AUTO
}

export class LazyMapsAPILoaderConfig {
apiKey: string = null;
hostAndPath: string = 'maps.googleapis.com/maps/api/js';
protocol: GoogleMapsScriptProtocol = GoogleMapsScriptProtocol.HTTPS;
}

const DEFAULT_CONFIGURATION = new LazyMapsAPILoaderConfig();

@Injectable()
export class LazyMapsAPILoader extends MapsAPILoader {
private _scriptLoadingPromise: Promise<void>;

constructor(@Optional() private _config: LazyMapsAPILoaderConfig = DEFAULT_CONFIGURATION) {
super();
}

load(): Promise<void> {
if (this._scriptLoadingPromise) {
return this._scriptLoadingPromise;
}

const script = document.createElement('script');
script.type = 'text/javascript';
script.async = true;
script.defer = true;
const callbackName: string = `angular2googlemaps${new Date().getMilliseconds()}`;
script.src = this._getScriptSrc(callbackName);

this._scriptLoadingPromise = new Promise<void>((resolve: Function, reject: Function) => {
(<any>window)[callbackName] = () => { resolve(); };

script.onerror = (error: Event) => { reject(error); };
});

document.body.appendChild(script);
return this._scriptLoadingPromise;
}

private _getScriptSrc(callbackName: string): string {
let protocolType: GoogleMapsScriptProtocol =
(this._config && this._config.protocol) || DEFAULT_CONFIGURATION.protocol;
let protocol: string;

switch (protocolType) {
case GoogleMapsScriptProtocol.AUTO:
protocol = '';
break;
case GoogleMapsScriptProtocol.HTTP:
protocol = 'http:';
break;
case GoogleMapsScriptProtocol.HTTPS:
protocol = 'https:';
break;
}

const hostAndPath: string =
(this._config && this._config.hostAndPath) || DEFAULT_CONFIGURATION.hostAndPath;
const apiKey: string = (this._config && this._config.apiKey) || DEFAULT_CONFIGURATION.apiKey;
const queryParams: {[key: string]: string} = {};
if (apiKey) {
queryParams['key'] = apiKey;
}
queryParams['callback'] = callbackName;
const queryParamsString: string =
Object.keys(queryParams)
.map(
(key: string, index: number) =>
index === 0 ? `?${key}=${queryParams[key]}` : `&${key}=${queryParams[key]}`)
.join('');
return `${protocol}//${hostAndPath}${queryParamsString}`;
}
}
6 changes: 6 additions & 0 deletions src/services/maps_api_loader/maps_api_loader.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import {Injectable} from 'angular2/angular2';

@Injectable()
export abstract class MapsAPILoader {
abstract load(): Promise<void>;
}
16 changes: 16 additions & 0 deletions src/services/maps_api_loader/noop_maps_api_loader.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import {MapsAPILoader} from './maps_api_loader';

/**
* When using the NoOpMapsAPILoader, the Google Maps API must be added to the page via a <script>
* Tag.
* It's important that the Google Maps API script gets loaded first on the page.
*/
export class NoOpMapsAPILoader implements MapsAPILoader {
load(): Promise<void> {
if (!(<any>window).google || !(<any>window).google.maps) {
throw new Error(
'Google Maps API not loaded on page. Make sure window.google.maps is available!');
}
return Promise.resolve();
};
}
30 changes: 16 additions & 14 deletions src/services/marker_manager.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,32 @@
import {Injectable} from 'angular2/angular2';
import {SebmGoogleMapMarker} from '../components/google_map_marker';
import {GoogleMapsAPIWrapperFactory, GoogleMapsAPIWrapper} from './google_maps_api_wrapper';
import {GoogleMapsAPIWrapper} from './google_maps_api_wrapper';

@Injectable()
export class MarkerManager {
private _markers: Map<SebmGoogleMapMarker, google.maps.Marker> =
new Map<SebmGoogleMapMarker, google.maps.Marker>();
private _mapsAPI: GoogleMapsAPIWrapper;
private _markers: Map<SebmGoogleMapMarker, Promise<google.maps.Marker>> =
new Map<SebmGoogleMapMarker, Promise<google.maps.Marker>>();

constructor(f: GoogleMapsAPIWrapperFactory) { this._mapsAPI = f.getInstance(); }
constructor(private _mapsWrapper: GoogleMapsAPIWrapper) {}

deleteMarker(marker: SebmGoogleMapMarker) {
console.log(this._markers.values());
this._markers.get(marker).setMap(null);
deleteMarker(marker: SebmGoogleMapMarker): Promise<void> {
let promise = this._markers.get(marker).then((m: google.maps.Marker) => m.setMap(null));
this._markers.delete(marker);
return promise;
}

updateMarkerPosition(marker: SebmGoogleMapMarker) {
this._markers.get(marker).setPosition({lat: marker.latitude, lng: marker.longitude});
updateMarkerPosition(marker: SebmGoogleMapMarker): Promise<void> {
return this._markers.get(marker).then(
(m: google.maps.Marker) => m.setPosition({lat: marker.latitude, lng: marker.longitude}));
}

updateTitle(marker: SebmGoogleMapMarker) { this._markers.get(marker).setTitle(marker.title); }
updateTitle(marker: SebmGoogleMapMarker): Promise<void> {
return this._markers.get(marker).then((m: google.maps.Marker) => m.setTitle(marker.title));
}

addMarker(marker: SebmGoogleMapMarker) {
let newMarker: google.maps.Marker =
this._mapsAPI.createMarker({position: {lat: marker.latitude, lng: marker.longitude}});
this._markers.set(marker, newMarker);
const markerPromise =
this._mapsWrapper.createMarker({position: {lat: marker.latitude, lng: marker.longitude}});
this._markers.set(marker, markerPromise);
}
}

0 comments on commit d05e6d3

Please sign in to comment.