diff --git a/package.json b/package.json index c0331dca1..7bee425fa 100644 --- a/package.json +++ b/package.json @@ -91,7 +91,8 @@ "tslint-loader": "^3.4.3", "typescript": "~2.2.0", "webpack": "2.3.3", - "zone.js": "^0.8.4" + "zone.js": "^0.8.4", + "js-marker-clusterer": "^1.0.0" }, "jspm": { "jspmNodeConversion": false, diff --git a/packages/clusterer/clusterer.module.ts b/packages/clusterer/clusterer.module.ts new file mode 100644 index 000000000..69c01bf2b --- /dev/null +++ b/packages/clusterer/clusterer.module.ts @@ -0,0 +1,13 @@ +import {NgModule} from '@angular/core'; +import {AgmCoreModule} from '../core'; +import {AgmMarkerCluster} from './directives/cluster'; +import {ClusterManager} from './services/managers/cluster-manager'; + +@NgModule({ + imports: [AgmCoreModule], + declarations: [AgmMarkerCluster], + exports: [AgmMarkerCluster, AgmCoreModule], + providers: [ClusterManager], +}) +export class AgmClustererModule { +} diff --git a/packages/clusterer/directives.ts b/packages/clusterer/directives.ts new file mode 100644 index 000000000..585ce914d --- /dev/null +++ b/packages/clusterer/directives.ts @@ -0,0 +1 @@ +export {AgmMarkerCluster} from './directives/cluster'; diff --git a/packages/clusterer/directives/cluster.ts b/packages/clusterer/directives/cluster.ts new file mode 100644 index 000000000..fb29ad85d --- /dev/null +++ b/packages/clusterer/directives/cluster.ts @@ -0,0 +1,124 @@ +import {Directive, Input, OnDestroy, OnChanges, OnInit, SimpleChange} from '@angular/core'; + +import {ClusterManager} from '../services/managers/cluster-manager'; +import {MarkerManager} from '../../core/services/managers/marker-manager'; + +import {IClusterOptions, IClusterStyle} from '../services/google-clusterer-types'; + +/** + * AgmMarkerCluster clusters map marker if they are near together + * + * ### Example + * ```typescript + * import { Component } from '@angular/core'; + * + * @Component({ + * selector: 'my-map-cmp', + * styles: [` + * .agm-map-container { + * height: 300px; + * } + * `], + * template: ` + * + * + * + * + * + * + * + * + * ` + * }) + * ``` + */ +@Directive({ + selector: 'agm-cluster', + providers: [ClusterManager, {provide: MarkerManager, useExisting: ClusterManager}] +}) +export class AgmMarkerCluster implements OnDestroy, OnChanges, OnInit, IClusterOptions { + /** + * The grid size of a cluster in pixels + */ + @Input() gridSize: number; + + /** + * The maximum zoom level that a marker can be part of a cluster. + */ + @Input() maxZoom: number; + + /** + * Whether the default behaviour of clicking on a cluster is to zoom into it. + */ + @Input() zoomOnClick: boolean; + + /** + * Whether the center of each cluster should be the average of all markers in the cluster. + */ + @Input() averageCenter: boolean; + + /** + * The minimum number of markers to be in a cluster before the markers are hidden and a count is shown. + */ + @Input() minimumClusterSize: number; + + /** + * An object that has style properties. + */ + @Input() styles: IClusterStyle; + + @Input() imagePath: string; + @Input() imageExtension: string; + + constructor(private cluster: ClusterManager) {} + + /** @internal */ + ngOnDestroy() { + this.cluster.clearMarkers(); + } + + /** @internal */ + ngOnChanges(changes: {[key: string]: SimpleChange }) { + if (changes['gridSize']) { + this.cluster.setGridSize(this); + } + if (changes['maxZoom']) { + this.cluster.setMaxZoom(this); + } + if (changes['styles']) { + this.cluster.setStyles(this); + } + if (changes['zoomOnClick']) { + this.cluster.setZoomOnClick(this); + } + if (changes['averageCenter']) { + this.cluster.setAverageCenter(this); + } + if (changes['minimumClusterSize']) { + this.cluster.setMinimumClusterSize(this); + } + if (changes['styles']) { + this.cluster.setStyles(this); + } + if (changes['imagePath']) { + this.cluster.setImagePath(this); + } + if (changes['imageExtension']) { + this.cluster.setImageExtension(this); + } + } + + /** @internal */ + ngOnInit() { + this.cluster.init({ + gridSize: this.gridSize, + maxZoom: this.maxZoom, + zoomOnClick: this.zoomOnClick, + averageCenter: this.averageCenter, + minimumClusterSize: this.minimumClusterSize, + styles: this.styles, + imagePath: this.imagePath, + imageExtension: this.imageExtension, + }); + } +} diff --git a/packages/clusterer/index.ts b/packages/clusterer/index.ts new file mode 100644 index 000000000..431c481f1 --- /dev/null +++ b/packages/clusterer/index.ts @@ -0,0 +1,8 @@ +// main modules +export * from './directives'; +export * from './services'; + +// core module +// we explicitly export the module here to prevent this Ionic 2 bug: +// http://stevemichelotti.com/integrate-angular-2-google-maps-into-ionic-2/ +export {AgmClustererModule} from './clusterer.module'; diff --git a/packages/clusterer/package.json b/packages/clusterer/package.json new file mode 100644 index 000000000..9b6d89df1 --- /dev/null +++ b/packages/clusterer/package.json @@ -0,0 +1,6 @@ +{ + "name": "@agm/clusterer-src", + "dependencies": { + "@agm/core": "file:../core" + } +} diff --git a/packages/clusterer/package.tpl.json b/packages/clusterer/package.tpl.json new file mode 100644 index 000000000..f6f0260ac --- /dev/null +++ b/packages/clusterer/package.tpl.json @@ -0,0 +1,29 @@ +{ + "name": "@agm/clusterer", + "version": "INSERT_HERE_VIA_BUILD_PROCESS", + "private": true, + "description": "Angular Google Maps (AGM) package for Snazzy Info Window support", + "repository": { + "type": "git", + "url": "git+https://github.com/SebastianM/angular-google-maps.git" + }, + "keywords": [ + "angular-google-maps", + "angular", + "js-marker-clusterer", + "info-window", + "google-maps", + "agm" + ], + "author": "Jens Fahnenbruck ", + "license": "MIT", + "bugs": { + "url": "https://github.com/SebastianM/angular-google-maps/issues" + }, + "peerDependencies": { + "@angular/core": "^4.0.0", + "@agm/core": "^1.0.0-beta.0", + "js-marker-clusterer": "^1.0.0" + }, + "homepage": "https://github.com/SebastianM/angular-google-maps#readme" +} diff --git a/packages/clusterer/services.ts b/packages/clusterer/services.ts new file mode 100644 index 000000000..06f16a130 --- /dev/null +++ b/packages/clusterer/services.ts @@ -0,0 +1 @@ +export {ClusterManager} from './services/managers/cluster-manager'; diff --git a/packages/clusterer/services/google-clusterer-types.ts b/packages/clusterer/services/google-clusterer-types.ts new file mode 100644 index 000000000..e0426255e --- /dev/null +++ b/packages/clusterer/services/google-clusterer-types.ts @@ -0,0 +1,114 @@ +import {Marker, GoogleMap, LatLngBounds} from '../../core/services/google-maps-types'; + +export interface CalculatorResult { + text: string; + index: number; +} + +export type CalculateFunction = (marker: Marker[], count: number) => CalculatorResult; + +export interface IMarkerClusterer { + zoomOnClick_: boolean; + averageCenter_: boolean; + imagePath_: string; + minimumClusterSize_: number; + imageExtension_: string; + new(map: GoogleMap, marker: Marker[], options: IClusterOptions): IMarkerClusterer; + addMarker(marker: Marker, noDraw?: boolean): void; + addMarkers(markers: Marker[], noDraw?: boolean): void; + clearMarkers(): void; + getCalculator(): CalculateFunction; + getExtendedBounds(bounds: LatLngBounds): LatLngBounds; + getGridSize(): number; + getMap(): GoogleMap; + getMarkers(): Marker[]; + getStyles(): IClusterStyle; + getTotalClusters(): number; + getTotalMarkers(): Marker[]; + isZoomOnClick(): boolean; + redraw(): void; + removeMarker(marker: Marker): boolean; + resetViewport(): void; + setCalculator(calculator: CalculateFunction): void; + setGridSize(size: number): void; + setMap(map: GoogleMap): void; + setMaxZoom(maxZoom: number): void; + setStyles(styles: IClusterStyle): void; +} + +export interface IClusterOptions { + /** + * The grid size of a cluster in pixels. + */ + gridSize?: number; + + /** + * The maximum zoom level that a marker can be part of a cluster. + */ + maxZoom?: number; + + /** + * Whether the default behaviour of clicking on a cluster is to zoom into it. + */ + zoomOnClick?: boolean; + + /** + * Whether the center of each cluster should be the average of all markers in the cluster. + */ + averageCenter?: boolean; + + /** + * The minimum number of markers to be in a cluster before the markers are hidden and a count is shown. + */ + minimumClusterSize?: number; + + /** + * An object that has style properties. + */ + styles?: IClusterStyle; + + imagePath?: string; + imageExtension?: string; +} + +export interface IClusterStyle { + /** + * The image url. + */ + url?: string; + + /** + * The image height. + */ + height?: number; + + /** + * The image width. + */ + width?: number; + + /** + * The anchor position of the label text. + */ + anchor?: [number, number]; + + /** + * The text color. + */ + textColor?: string; + + /** + * The text size. + */ + textSize?: number; + + /** + * The position of the backgound x, y. + */ + backgroundPosition?: string; + + /** + * The anchor position of the icon x, y. + */ + iconAnchor?: [number, number]; +} diff --git a/packages/clusterer/services/managers/cluster-manager.spec.ts b/packages/clusterer/services/managers/cluster-manager.spec.ts new file mode 100644 index 000000000..b44e31717 --- /dev/null +++ b/packages/clusterer/services/managers/cluster-manager.spec.ts @@ -0,0 +1,196 @@ +import {NgZone} from '@angular/core'; +import {TestBed, async, inject} from '@angular/core/testing'; + +import {AgmMarker} from '../../../core/directives/marker'; +import {GoogleMapsAPIWrapper} from '../../../core/services/google-maps-api-wrapper'; +import {Marker} from '../../../core/services/google-maps-types'; +import {ClusterManager} from './cluster-manager'; + +describe('ClusterManager', () => { + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [ + {provide: NgZone, useFactory: () => new NgZone({enableLongStackTrace: true})}, + ClusterManager, { + provide: GoogleMapsAPIWrapper, + useValue: jasmine.createSpyObj('GoogleMapsAPIWrapper', ['createMarker']) + } + ] + }); + }); + + describe('Create a new marker', () => { + it('should call the mapsApiWrapper when creating a new marker', + inject( + [ClusterManager, GoogleMapsAPIWrapper], + (clusterManager: ClusterManager, apiWrapper: GoogleMapsAPIWrapper) => { + const newMarker = new AgmMarker(clusterManager); + newMarker.latitude = 34.4; + newMarker.longitude = 22.3; + newMarker.label = 'A'; + clusterManager.addMarker(newMarker); + + expect(apiWrapper.createMarker).toHaveBeenCalledWith({ + position: {lat: 34.4, lng: 22.3}, + label: 'A', + draggable: false, + icon: undefined, + opacity: 1, + visible: true, + zIndex: 1, + title: undefined, + clickable: true + }, false); + })); + }); + + describe('Delete a marker', () => { + it('should set the map to null when deleting a existing marker', + inject( + [ClusterManager, GoogleMapsAPIWrapper], + (clusterManager: ClusterManager, apiWrapper: GoogleMapsAPIWrapper) => { + const newMarker = new AgmMarker(clusterManager); + newMarker.latitude = 34.4; + newMarker.longitude = 22.3; + newMarker.label = 'A'; + + const markerInstance: Marker = jasmine.createSpyObj('Marker', ['setMap']); + (apiWrapper.createMarker).and.returnValue(Promise.resolve(markerInstance)); + + clusterManager.addMarker(newMarker); + clusterManager.deleteMarker(newMarker).then( + () => { expect(markerInstance.setMap).toHaveBeenCalledWith(null); }); + })); + }); + + describe('set marker icon', () => { + it('should update that marker via setIcon method when the markerUrl changes', + async(inject( + [ClusterManager, GoogleMapsAPIWrapper], + (markerManager: ClusterManager, apiWrapper: GoogleMapsAPIWrapper) => { + const newMarker = new AgmMarker(markerManager); + newMarker.latitude = 34.4; + newMarker.longitude = 22.3; + newMarker.label = 'A'; + + const markerInstance: Marker = jasmine.createSpyObj('Marker', ['setMap', 'setIcon']); + (apiWrapper.createMarker).and.returnValue(Promise.resolve(markerInstance)); + + markerManager.addMarker(newMarker); + expect(apiWrapper.createMarker).toHaveBeenCalledWith({ + position: {lat: 34.4, lng: 22.3}, + label: 'A', + draggable: false, + icon: undefined, + opacity: 1, + visible: true, + zIndex: 1, + title: undefined, + clickable: true + }, false); + const iconUrl = 'http://angular-maps.com/icon.png'; + newMarker.iconUrl = iconUrl; + return markerManager.updateIcon(newMarker).then( + () => { expect(markerInstance.setIcon).toHaveBeenCalledWith(iconUrl); }); + }))); + }); + + describe('set marker opacity', () => { + it('should update that marker via setOpacity method when the markerOpacity changes', + async(inject( + [ClusterManager, GoogleMapsAPIWrapper], + (markerManager: ClusterManager, apiWrapper: GoogleMapsAPIWrapper) => { + const newMarker = new AgmMarker(markerManager); + newMarker.latitude = 34.4; + newMarker.longitude = 22.3; + newMarker.label = 'A'; + + const markerInstance: Marker = + jasmine.createSpyObj('Marker', ['setMap', 'setOpacity']); + (apiWrapper.createMarker).and.returnValue(Promise.resolve(markerInstance)); + + markerManager.addMarker(newMarker); + expect(apiWrapper.createMarker).toHaveBeenCalledWith({ + position: {lat: 34.4, lng: 22.3}, + label: 'A', + draggable: false, + icon: undefined, + visible: true, + opacity: 1, + zIndex: 1, + title: undefined, + clickable: true + }, false); + const opacity = 0.4; + newMarker.opacity = opacity; + return markerManager.updateOpacity(newMarker).then( + () => { expect(markerInstance.setOpacity).toHaveBeenCalledWith(opacity); }); + }))); + }); + + describe('set visible option', () => { + it('should update that marker via setVisible method when the visible changes', + async(inject( + [ClusterManager, GoogleMapsAPIWrapper], + (markerManager: ClusterManager, apiWrapper: GoogleMapsAPIWrapper) => { + const newMarker = new AgmMarker(markerManager); + newMarker.latitude = 34.4; + newMarker.longitude = 22.3; + newMarker.label = 'A'; + newMarker.visible = false; + + const markerInstance: Marker = + jasmine.createSpyObj('Marker', ['setMap', 'setVisible']); + (apiWrapper.createMarker).and.returnValue(Promise.resolve(markerInstance)); + + markerManager.addMarker(newMarker); + expect(apiWrapper.createMarker).toHaveBeenCalledWith({ + position: {lat: 34.4, lng: 22.3}, + label: 'A', + draggable: false, + icon: undefined, + visible: false, + opacity: 1, + zIndex: 1, + title: undefined, + clickable: true + }, false); + newMarker.visible = true; + return markerManager.updateVisible(newMarker).then( + () => { expect(markerInstance.setVisible).toHaveBeenCalledWith(true); }); + }))); + }); + + describe('set zIndex option', () => { + it('should update that marker via setZIndex method when the zIndex changes', + async(inject( + [ClusterManager, GoogleMapsAPIWrapper], + (markerManager: ClusterManager, apiWrapper: GoogleMapsAPIWrapper) => { + const newMarker = new AgmMarker(markerManager); + newMarker.latitude = 34.4; + newMarker.longitude = 22.3; + newMarker.label = 'A'; + newMarker.visible = false; + + const markerInstance: Marker = jasmine.createSpyObj('Marker', ['setMap', 'setZIndex']); + (apiWrapper.createMarker).and.returnValue(Promise.resolve(markerInstance)); + + markerManager.addMarker(newMarker); + expect(apiWrapper.createMarker).toHaveBeenCalledWith({ + position: {lat: 34.4, lng: 22.3}, + label: 'A', + draggable: false, + icon: undefined, + visible: false, + opacity: 1, + zIndex: 1, + title: undefined, + clickable: true + }, false); + const zIndex = 10; + newMarker.zIndex = zIndex; + return markerManager.updateZIndex(newMarker).then( + () => { expect(markerInstance.setZIndex).toHaveBeenCalledWith(zIndex); }); + }))); + }); +}); diff --git a/packages/clusterer/services/managers/cluster-manager.ts b/packages/clusterer/services/managers/cluster-manager.ts new file mode 100644 index 000000000..a27c2b823 --- /dev/null +++ b/packages/clusterer/services/managers/cluster-manager.ts @@ -0,0 +1,195 @@ +import {Injectable, NgZone} from '@angular/core'; + +import 'js-marker-clusterer'; + +import {MarkerManager} from '../../../core/services/managers/marker-manager'; +import {GoogleMapsAPIWrapper} from '../../../core/services/google-maps-api-wrapper'; +import {AgmMarker} from '../../../core/directives/marker'; +import {AgmMarkerCluster} from './../../directives/cluster'; +// tslint:disable-next-line: no-use-before-declare +import {Marker} from '../../../core/services/google-maps-types'; +import {IMarkerClusterer, IClusterOptions} from '../google-clusterer-types'; + +declare var MarkerClusterer: IMarkerClusterer; + +class Deferred { + private _promise: Promise; + private fate: 'resolved' | 'unresolved'; + private state: 'pending' | 'fulfilled' | 'rejected'; + private _resolve: Function; + private _reject: Function; + + constructor() { + this.state = 'pending'; + this.fate = 'unresolved'; + this._promise = new Promise((resolve, reject) => { + this._resolve = resolve; + this._reject = reject; + }); + + this.promise.then( + () => this.state = 'fulfilled', + () => this.state = 'rejected', + ); + } + + get promise(): Promise { + return this._promise; + } + + resolve(value?: any) { + if (this.fate === 'resolved') { + throw 'Deferred cannot be resolved twice'; + } + this.fate = 'resolved'; + this._resolve(value); + } + + reject(reason?: any) { + if (this.fate === 'resolved') { + throw 'Deferred cannot be resolved twice'; + } + this.fate = 'resolved'; + this._reject(reason); + } + + isResolved() { + return this.fate === 'resolved'; + } + + isPending() { + return this.state === 'pending'; + } + + isFulfilled() { + return this.state === 'fulfilled'; + } + + isRejected() { + return this.state === 'rejected'; + } +} + +@Injectable() +export class ClusterManager extends MarkerManager { + private _deferred: Deferred; + + constructor(protected _mapsWrapper: GoogleMapsAPIWrapper, protected _zone: NgZone) { + super(_mapsWrapper, _zone); + this._deferred = new Deferred(); + } + + init(options: IClusterOptions): void { + this._mapsWrapper.getNativeMap().then(map => { + const clusterer = new MarkerClusterer(map, [], options); + this._deferred.resolve(clusterer); + return clusterer; + }); + } + + addMarker(marker: AgmMarker): void { + const clusterPromise: Promise = this._deferred.promise; + const markerPromise = this._mapsWrapper + .createMarker({ + position: { + lat: marker.latitude, + lng: marker.longitude + }, + label: marker.label, + draggable: marker.draggable, + icon: marker.iconUrl, + opacity: marker.opacity, + visible: marker.visible, + zIndex: marker.zIndex, + title: marker.title, + clickable: marker.clickable, + }, false); + + Promise + .all([clusterPromise, markerPromise]) + .then(([cluster, marker]) => { + return cluster.addMarker(marker); + }); + this._markers.set(marker, markerPromise); + } + + deleteMarker(marker: AgmMarker): Promise { + const m = this._markers.get(marker); + if (m == null) { + // marker already deleted + return Promise.resolve(); + } + return m.then((m: Marker) => { + this._zone.run(() => { + this._deferred.promise.then(cluster => { + cluster.removeMarker(m); + this._markers.delete(marker); + }); + }); + }); + } + + clearMarkers(): Promise { + return this._deferred.promise.then(cluster => { + cluster.clearMarkers(); + }); + } + + setGridSize(c: AgmMarkerCluster): void { + this._deferred.promise.then(cluster => { + cluster.setGridSize(c.gridSize); + }); + } + + setMaxZoom(c: AgmMarkerCluster): void { + this._deferred.promise.then(cluster => { + cluster.setMaxZoom(c.maxZoom); + }); + } + + setStyles(c: AgmMarkerCluster): void { + this._deferred.promise.then(cluster => { + cluster.setStyles(c.styles); + }); + } + + setZoomOnClick(c: AgmMarkerCluster): void { + this._deferred.promise.then(cluster => { + if (c.zoomOnClick !== undefined) { + cluster.zoomOnClick_ = c.zoomOnClick; + } + }); + } + + setAverageCenter(c: AgmMarkerCluster): void { + this._deferred.promise.then(cluster => { + if (c.averageCenter !== undefined) { + cluster.averageCenter_ = c.averageCenter; + } + }); + } + + setImagePath(c: AgmMarkerCluster): void { + this._deferred.promise.then(cluster => { + if (c.imagePath !== undefined) { + cluster.imagePath_ = c.imagePath; + } + }); + } + + setMinimumClusterSize(c: AgmMarkerCluster): void { + this._deferred.promise.then(cluster => { + if (c.minimumClusterSize !== undefined) { + cluster.minimumClusterSize_ = c.minimumClusterSize; + } + }); + } + + setImageExtension(c: AgmMarkerCluster): void { + this._deferred.promise.then(cluster => { + if (c.imageExtension !== undefined) { + cluster.imageExtension_ = c.imageExtension; + } + }); + } +} diff --git a/packages/core/services/google-maps-api-wrapper.ts b/packages/core/services/google-maps-api-wrapper.ts index 7a611859a..006cd82e6 100644 --- a/packages/core/services/google-maps-api-wrapper.ts +++ b/packages/core/services/google-maps-api-wrapper.ts @@ -39,10 +39,12 @@ export class GoogleMapsAPIWrapper { /** * Creates a google map marker with the map context */ - createMarker(options: mapTypes.MarkerOptions = {}): + createMarker(options: mapTypes.MarkerOptions = {}, addToMap: boolean = true): Promise { return this._map.then((map: mapTypes.GoogleMap) => { - options.map = map; + if (addToMap) { + options.map = map; + } return new google.maps.Marker(options); }); } diff --git a/packages/core/services/managers/marker-manager.ts b/packages/core/services/managers/marker-manager.ts index 518ce79d7..73f307cf8 100644 --- a/packages/core/services/managers/marker-manager.ts +++ b/packages/core/services/managers/marker-manager.ts @@ -9,10 +9,10 @@ import {Marker} from './../google-maps-types'; @Injectable() export class MarkerManager { - private _markers: Map> = + protected _markers: Map> = new Map>(); - constructor(private _mapsWrapper: GoogleMapsAPIWrapper, private _zone: NgZone) {} + constructor(protected _mapsWrapper: GoogleMapsAPIWrapper, protected _zone: NgZone) {} deleteMarker(marker: AgmMarker): Promise { const m = this._markers.get(marker); diff --git a/rollup.core.config.js b/rollup.core.config.js index 9426e3dea..ca248e866 100644 --- a/rollup.core.config.js +++ b/rollup.core.config.js @@ -10,6 +10,7 @@ export default { '@angular/compiler': 'ng.compiler', '@angular/platform-browser': 'ng.platformBrowser', '@angular/platform-browser-dynamic': 'ng.platformBrowserDynamic', + 'js-marker-clusterer': 'MarkerClusterer', 'rxjs/Subject': 'Rx', 'rxjs/observable/PromiseObservable': 'Rx', 'rxjs/operator/toPromise': 'Rx.Observable.prototype', @@ -17,5 +18,5 @@ export default { 'rxjs/Rx': 'Rx' }, context: 'window', - external: ['rxjs', '@angular/core', 'rxjs/Observable'] + external: ['rxjs', '@angular/core', 'rxjs/Observable', 'js-marker-clusterer'] } diff --git a/scripts/packages.js b/scripts/packages.js index ba4b5a1f3..bc95975b6 100644 --- a/scripts/packages.js +++ b/scripts/packages.js @@ -1,7 +1,8 @@ // NPM packages (without the org name) that we publish const packages = [ 'core', - 'snazzy-info-window' + 'snazzy-info-window', + 'clusterer', ]; -module.exports = packages; \ No newline at end of file +module.exports = packages; diff --git a/yarn.lock b/yarn.lock index 12c21a31d..6666293a6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2183,6 +2183,10 @@ jodid25519@^1.0.0: dependencies: jsbn "~0.1.0" +js-marker-clusterer@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/js-marker-clusterer/-/js-marker-clusterer-1.0.0.tgz#c94be85ae8896819e51c131f891dd2b55558dd17" + js-tokens@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.1.tgz#08e9f132484a2c45a30907e9dc4d5567b7f114d7" @@ -4024,9 +4028,9 @@ webpack-sources@^0.2.3: source-list-map "^1.1.1" source-map "~0.5.3" -webpack@2.3.2: - version "2.3.2" - resolved "https://registry.yarnpkg.com/webpack/-/webpack-2.3.2.tgz#7d521e6f0777a3a58985c69425263fdfe977b458" +webpack@2.3.3: + version "2.3.3" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-2.3.3.tgz#eecc083c18fb7bf958ea4f40b57a6640c5a0cc78" dependencies: acorn "^4.0.4" acorn-dynamic-import "^2.0.0"