define([
'aeris/util',
'aeris/maps/strategy/abstractstrategy',
'gmaps-markerclusterer-plus',
'aeris/util/gmaps',
'aeris/maps/strategy/markers/markerclusterstyletemplate'
], function(_, AbstractStrategy, MarkerClusterer, mapUtil, markerClusterStyleTemplate) {
if (document.readyState === 'complete') {
injectClusterStyles();
}
else {
document.addEventListener('DOMContentLoaded', injectClusterStyles, false);
}
function injectClusterStyles() {
// Include styles for marker clusters.
// Injects a <style> object into the DOM.
var node = document.createElement('style');
node.innerHTML = markerClusterStyleTemplate();
document.body.appendChild(node);
}
/**
* Strategy for rendering a collection of markers in a cluster.
*
* @class aeris.maps.gmaps.markers.MarkerCluster
* @extends aeris.maps.gmaps.AbstractStrategy
*
* @constructor
* @override
*
* @param {aeris.Collection} object A collection of {aeris.maps.markers.Marker} objects.
*
* @param {Object=} opt_options
* @param {Function=} opt_options.MarkerClusterer
*/
var MarkerClusterStrategy = function(object, opt_options) {
var options = _.defaults(opt_options || {}, {
MarkerClusterer: MarkerClusterer
});
/**
* A hash of MarkerClusterer objects,
* referenced by group name.
*
* eg.
* {
* snow: {MarkerClusterer},
* rain: {MarkerClusterer}
* }
*
* @property view
* @type {Object.<string,MarkerClusterer>}
*/
/**
* When a MarkerClusterer instance is created.
*
* @event clusterer:create
* @param {MarkerClusterer} clusterer
*/
/**
* When a MarkerClusterer instance is
* added to the view.
*
* @event clusterer:add
* @param {MarkerClusterer} clusterer
* @param {string} groupName
*/
/**
* When a MarkerClusterer instance is
* removed from the view.
*
* @event clusterer:remove
* @param {MarkerClusterer} clusterer
* @param {string} groupName
*/
/**
* Constructor for the
* MarkerClusterer object.
*
* @type {Function}
* @private
* @property MarkerClusterer_
*/
this.MarkerClusterer_ = options.MarkerClusterer;
AbstractStrategy.apply(this, arguments);
this.bindToMarkerCollection_();
// Proxy clusterer view events
this.listenTo(this, {
'clusterer:add': this.proxyClustererEvents_,
'clusterer:remove': function(clusterer) {
this.googleEvents_.stopListening(clusterer);
}
});
};
_.inherits(MarkerClusterStrategy, AbstractStrategy);
/**
* @method bindToMarkerCollection_
* @private
*/
MarkerClusterStrategy.prototype.bindToMarkerCollection_ = function() {
// Debounce event handlers.
// Resolves significant performance issues
// when data is reset on very large collections (~500+ models)
var resetDebounced = _.debounce(function() {
this.resetClusters();
}, 500);
var repaintDebounced = _.debounce(function() {
this.repaint();
}, 500);
// Bind to marker collection object
this.listenTo(this.object_, {
add: function(marker, obj, opts) {
this.addMarker(marker);
},
remove: resetDebounced,
reset: resetDebounced,
'change': repaintDebounced
});
};
/**
*
* @private
* @return {Object}
* @method createView_
*/
MarkerClusterStrategy.prototype.createView_ = function() {
this.view_ = {};
this.addMarkers(this.object_.models);
return this.view_;
};
/**
* Sets the map on all {MarkerClusterer} objects.
*
* @override
* @method setMap
*/
MarkerClusterStrategy.prototype.setMap = function(aerisMap) {
AbstractStrategy.prototype.setMap.apply(this, arguments);
_.invoke(this.getView(), 'setMap', this.mapView_);
};
/**
* Sets the map to null on all {MarkerClusterer} objects.
*
* @override
* @method beforeRemove_
*/
MarkerClusterStrategy.prototype.beforeRemove_ = function() {
_.invoke(this.getView(), 'setMap', null);
// Remove all child markers from the map.
//
// BUG FIX: removing child MapObjects is
// handled by our MapObjectCollection
// However, our MarkerClusterer is also trying to
// add and remove child markers from the map.
// Because of some funky timing, we always end up with
// one marker left on the map.
_.defer(function() {
// Check map is really still null after call stack
// has been resolved.
if (!this.object_.getMap()) {
_.invoke(this.getAllMarkers_(), 'setMap', null);
}
}.bind(this));
};
/**
* Add a set of markers to the
* appropriate {MarkerClusterer} views.
*
* @param {Array.<aeris.maps.markers.Marker>} markers
* @method addMarkers
*/
MarkerClusterStrategy.prototype.addMarkers = function(markers) {
_.each(markers, function(marker) {
this.addMarker(marker);
}, this);
};
/**
* Add a marker to the appropriate
* {MarkerClusterer} view.
*
* @fires 'clusterer:create'
* @fires 'clusterer:add'
*
* @param {aeris.maps.markers.Marker} marker
* @method addMarker
*/
MarkerClusterStrategy.prototype.addMarker = function(marker) {
var markerView;
var groupName = this.getMarkerGroup_(marker);
// Create the clusterer, if one doesn't already
// exist for the marker's group.
if (!this.getClusterer(groupName)) {
this.addClusterer_(this.createClusterer_(groupName), groupName);
}
// Add the marker to its group's clusterer
markerView = marker.getView();
this.getClusterer(groupName).addMarker(markerView);
};
/**
* Remove a marker from it's clusterer.
*
* @param {aeris.maps.markers.Marker} marker
* @method removeMarker
*/
MarkerClusterStrategy.prototype.removeMarker = function(marker) {
var groupName = this.getMarkerGroup_(marker);
var clusterer = this.getClusterer(groupName);
if (clusterer) {
clusterer.removeMarker(marker.getView());
}
};
/**
* Remove a set of markers from their clusterers.
*
* @param {Array.<aeris.maps.markers.Marker>} markers
* @method removeMarkers
*/
MarkerClusterStrategy.prototype.removeMarkers = function(markers) {
_.each(markers, function(marker) {
this.removeMarker(marker);
}, this);
};
/**
* Remove all markers from all clusterers.
*
* @fires 'clusterer:remove'
* @method clearClusters
*/
MarkerClusterStrategy.prototype.clearClusters = function() {
var view = this.getView();
// Clean up clusterer views
_.each(view, function(clusterer, groupName) {
clusterer.clearMarkers();
clusterer.setMap(null);
// Clean up reference to clusterer.
delete view[groupName];
this.trigger('clusterer:remove', clusterer, groupName);
}, this);
};
/**
* Refresh the clusters view,
* using our object's models.
* @method resetClusters
*/
MarkerClusterStrategy.prototype.resetClusters = function() {
this.clearClusters();
this.addMarkers(this.object_.models);
};
/**
* Redraws all MarkerClusterers.
* @method repaint
*/
MarkerClusterStrategy.prototype.repaint = function() {
_(this.getView()).invoke('repaint');
};
/**
* Get the group name for a marker,
* using the object's 'clusterBy' setting.
*
* @param {aeris.maps.markers.Marker} marker
* @return {string}
* @private
* @method getMarkerGroup_
*/
MarkerClusterStrategy.prototype.getMarkerGroup_ = function(marker) {
// Use the marker's type to separate clusters.
// If no marker type is defined,
// use a single default cluster name
return marker.getType() || MarkerClusterStrategy.SINGLE_CLUSTER_GROUPNAME;
};
/**
* Create a MarkerClusterer object.
*
* @fires 'clusterer:create'
*
* @param {string|undefined} groupName Required in order to set the cluster group styles.
* @param {Array.<aeris.maps.markers.Marker>} opt_markers
* @private
*
* @return {MarkerClusterer}
* @method createClusterer_
*/
MarkerClusterStrategy.prototype.createClusterer_ = function(groupName) {
var clusterer;
var clustererOptions = _.defaults({}, this.object_.getClusterOptions(), {
clusterClass: 'aeris-cluster',
styles: this.getClusterStyle_(groupName),
averageCenter: true,
zoomOnClick: true,
gridSize: 60,
maxZoom: 9,
minimumClusterSize: 3
});
clusterer = new this.MarkerClusterer_(
this.mapView_,
[],
clustererOptions
);
this.trigger('clusterer:create', clusterer);
return clusterer;
};
/**
* @method getClusterStyle_
* @private
*/
MarkerClusterStrategy.prototype.getClusterStyle_ = function(groupName) {
var style = _.clone(this.object_.getClusterStyle(groupName));
style.anchorIcon = [style.offsetX, style.offsetY];
return style;
};
/**
* Adds a MarkerClusterer with the given
* group name to the view.
*
* Will overwrite any existing clusterer
* with the same group name.
*
* @fires 'clusterer:add'
*
* @param {MarkerClusterer} clusterer
* @param {string} groupName
* @private
* @method addClusterer_
*/
MarkerClusterStrategy.prototype.addClusterer_ = function(clusterer, groupName) {
this.view_[groupName] = clusterer;
this.trigger('clusterer:add', clusterer, groupName);
};
/**
* Get a MarkerClusterer by group name.
*
* @param {string} groupName
* @return {MarkerClusterer}
* @method getClusterer
*/
MarkerClusterStrategy.prototype.getClusterer = function(groupName) {
return this.getView()[groupName];
};
/**
* Proxy events emitted by a
* {MarkerClusterer} object
* over to our {aeris.maps.MapObject}
*
* @param {MarkerClusterer} clusterer
* @private
* @method proxyClustererEvents_
*/
MarkerClusterStrategy.prototype.proxyClustererEvents_ = function(clusterer) {
var triggerClusterMouseEvent = _.bind(function(topic, cluster) {
var latLon = mapUtil.latLngToArray(cluster.getCenter());
this.object_.trigger('cluster:' + topic, latLon);
}, this);
this.googleEvents_.listenTo(clusterer, {
click: function(cluster) {
triggerClusterMouseEvent('click', cluster);
},
mouseout: function(cluster) {
triggerClusterMouseEvent('mouseout', cluster);
},
mouseover: function(cluster) {
triggerClusterMouseEvent('mouseover', cluster);
}
});
};
/**
* Return all marker views from all clusterers.
*
* @return {Array.<google.maps.Marker>}
* @private
* @method getAllMarkers_
*/
MarkerClusterStrategy.prototype.getAllMarkers_ = function() {
return _.reduce(this.getView(), function(memo, clusterer) {
return memo.concat(clusterer.getMarkers());
}, [], this);
};
/**
* @method destroy
*/
MarkerClusterStrategy.prototype.destroy = function() {
this.clearClusters();
// Put each marker view back on the map.
// --> we're destroying the clustering strategy,
// but we still want the markers to be rendered.
this.object_.each(function(markerObj) {
if (this.mapView_) {
markerObj.getView().setMap(this.mapView_);
}
}, this);
AbstractStrategy.prototype.destroy.call(this);
};
/**
* The group name used to reference
* a MarkerClusterer, when no 'clusterBy' option
* is set on the object.
*
* @static
* @type {string}
*/
MarkerClusterStrategy.SINGLE_CLUSTER_GROUPNAME = 'MARKERCLUSTERSTRATEGY_SINGLE_CLUSTER';
return MarkerClusterStrategy;
});
/**
* @for aeris.maps.markercollections.MarkerCollection
*/
/**
* @event cluster:click
* @param {aeris.maps.LatLon} latLon
*/
/**
* When the mouse enters a cluster.
*
* @event cluster:mouseover
* @param {aeris.maps.LatLon} latLon
*/
/**
* When the mouse exits a cluster.
*
* @event cluster:mouseout
* @param {aeris.maps.LatLon} latLon
*/