Fork me on GitHub
Show:

File: ../src/maps/leaflet/markers/markercluster.js

define([
  'aeris/util',
  'aeris/maps/abstractstrategy',
  'aeris/maps/strategy/util',
  'leaflet',
  'leaflet-markercluster',
  'aeris/maps/strategy/markers/clustericontemplate'
], function(_, AbstractStrategy, mapUtil, Leaflet, MarkerClusterGroup, clusterIconTemplate) {
  /**
   * A strategy for rendering clusters of markers
   * using Leaflet.
   *
   * @class aeris.maps.leaflet.MarkerCluster
   * @extends aeris.maps.AbstractStrategy
   *
   * @constructor
   */
  var MarkerCluster = function(markerCollection) {
    /**
     * A hash of MarkerClusterer objects,
     * referenced by group name.
     *
     * eg.
     *    {
     *      snow: {L.MarkerClusterGroup},
     *      rain: {L.MarkerClusterGroup}
     *    }
     *
     * @property view
     * @type {Object.<string,L.MarkerClusterGroup>}
     */

    AbstractStrategy.call(this, markerCollection);

    this.bindObjectToView_();
    this.addBulkMarkers_(this.object_.models);
  };
  _.inherits(MarkerCluster, AbstractStrategy);


  /**
   * @method createView_
   * @private
   */
  MarkerCluster.prototype.createView_ = function() {
    var view = {};

    // Create default cluster group
    view[MarkerCluster.SINGLE_CLUSTER_GROUPNAME_] = this.createCluster_(MarkerCluster.SINGLE_CLUSTER_GROUPNAME_);

    return view;
  };


  /**
   * @method setMap
   */
  MarkerCluster.prototype.setMap = function(map) {
    AbstractStrategy.prototype.setMap.call(this, map);

    // Add clusters to the map
    _.values(this.view_).
      forEach(function(view) {
        view.addTo(this.mapView_);
      }, this);

    // Add all markers to the clusters
    this.resetMarkers_(this.object_.models);
  };


  /**
   * @method beforeRemove_
   * @private
   */
  MarkerCluster.prototype.beforeRemove_ = function() {
    _.each(this.view_, function(clusterView) {
      this.mapView_.removeLayer(clusterView);
    }, this);
  };


  /**
   * @method createCluster_
   * @private
   * @param {string} type
   * @return {L.MarkerClusterGroup}
   */
  MarkerCluster.prototype.createCluster_ = function(type) {
    var cluster = new MarkerClusterGroup({
      iconCreateFunction: function(cluster) {
        return this.createIconForCluster_(cluster, type);
      }.bind(this)
    });

    this.proxyMouseEventsForCluster_(cluster);

    if (this.mapView_) {
      cluster.addTo(this.mapView_);
    }

    return cluster;
  };


  /**
   * @method createIconForCluster_
   * @private
   *
   * @param {L.MarkerCluster} cluster
   * @param {string} type Icon type category
   * @return {L.Icon}
   */
  MarkerCluster.prototype.createIconForCluster_ = function(cluster, type) {
    var clusterStyle = this.getClusterStyle_(cluster, type);

    return new L.DivIcon({
      html: this.createHtmlForCluster_(cluster, type),
      iconSize: new Leaflet.Point(clusterStyle.width, clusterStyle.height),
      iconAnchor: new Leaflet.Point(clusterStyle.offsetX, clusterStyle.offsetY),
      className: MarkerCluster.CLUSTER_CSS_CLASS_
    });
  };


  /**
   * @method createClusterHtml_
   * @private
   * @param {L.MarkerCluster} cluster
   * @param {string} type Icon type category
   * @return {string}
   */
  MarkerCluster.prototype.createHtmlForCluster_ = function(cluster, type) {
    var clusterStyle = this.getClusterStyle_(cluster, type);

    return clusterIconTemplate(_.extend({}, clusterStyle, {
      count: cluster.getChildCount()
    }));
  };


  /**
   * Chooses a cluster style based on the specified
   * cluster type, and the size of the cluster.
   *
   * @method getClusterStyle_
   * @private
   *
   * @param {L.MarkerCluster} cluster
   * @param {string} type
   * @return {Object=} Style options
   */
  MarkerCluster.prototype.getClusterStyle_ = function(cluster, type) {
    var clusterStyles = this.object_.getClusterStyle(type);
    var count = cluster.getChildCount();
    var index = -1;

    while (count !== 0) {
      count = parseInt(count / 10, 10);
      index++;
    }

    // Index resolves to:
    //      size < 10    ==>   0
    // 10 < size < 100   ==>   1
    // 100 < size < 1000 ==>   2
    // etc..
    index = Math.min(index, clusterStyles.length - 1);

    return clusterStyles[index];
  };


  /**
   * @method addBulkMarkers_
   * @param {Array.<aeris.maps.Marker>} markers
   * @private
   */
  MarkerCluster.prototype.addBulkMarkers_ = function(markers) {
    var markersByGroup = _.groupBy(markers, this.getMarkerType_, this);

    _.each(markersByGroup, function(markersInGroup, type) {
      var markerViews = markersInGroup.map(function(marker) {
        return marker.getView();
      });

      this.ensureClusterGroup_(type);

      markersInGroup.forEach(this.hideMarkerView_, this);

      this.view_[type].addLayers(markerViews);
    }, this);
  };


  /**
   * @method removeMarker_
   * @private
   * @param {aeris.maps.Marker} marker
   */
  MarkerCluster.prototype.removeMarker_ = function(marker) {
    this.getClusterForMarker_(marker).removeLayer(marker.getView());
  };


  /**
   * @method resetMarkers_
   * @private
   * @param {Array.<aeris.maps.Marker>=} opt_replacementMarkers
   */
  MarkerCluster.prototype.resetMarkers_ = function(opt_replacementMarkers) {
    _.invoke(this.view_, 'clearLayers');

    if (opt_replacementMarkers) {
      this.addBulkMarkers_(opt_replacementMarkers);
    }
  };


  /**
   * Hide a marker's view from the map,
   * without effecting the state of the marker object.
   *
   * @method hideMarkerView_
   * @private
   * @param {aeris.maps.Marker} marker
   */
  MarkerCluster.prototype.hideMarkerView_ = function(marker) {
    if (this.mapView_) {
      this.mapView_.removeLayer(marker.getView());
    }
  };


  /**
   * @method ensureClusterGroup_
   * @private
   * @param {string} type
   */
  MarkerCluster.prototype.ensureClusterGroup_ = function(type) {
    if (!this.view_[type]) {
      this.view_[type] = this.createCluster_(type);
    }
  };


  /**
   * @method getClusterForMarker_
   * @private
   * @param {aeris.maps.Marker} marker
   * @return {L.MarkerClusterGroup}
   */
  MarkerCluster.prototype.getClusterForMarker_ = function(marker) {
    var type = this.getMarkerType_(marker);
    return this.view_[type];
  };


  /**
   * @method getMarkerType_
   * @private
   * @param {aeris.maps.Marker} marker
   * @return {string} Marker group type
   */
  MarkerCluster.prototype.getMarkerType_ = function(marker) {
    return marker.getType() || MarkerCluster.SINGLE_CLUSTER_GROUPNAME_;
  };


  /**
   * @method bindObjectToView_
   * @private
   */
  MarkerCluster.prototype.bindObjectToView_ = function() {
    this.listenTo(this.object_, {
      'add': function() {
        this.addBulkMarkers_(this.object_.models);
      },
      'remove': this.removeMarker_,
      'reset': function() {
        this.resetMarkers_(this.object_.models);
      }
    });
  };


  /**
   * @method proxyMouseEvents_
   * @private
   * @param {L.MarkerClusterGroup} cluster
   */
  MarkerCluster.prototype.proxyMouseEventsForCluster_ = function(cluster) {
    cluster.on({
      'clusterclick': this.triggerMouseEvent_.bind(this, 'cluster:click'),
      'clustermouseover': this.triggerMouseEvent_.bind(this, 'cluster:mouseover'),
      'clustermouseout': this.triggerMouseEvent_.bind(this, 'cluster:mouseout')
    }, this);
  };


  /**
   * Trigger a click event on the {aeris.maps.markercollections.MarkerCollection}
   * object, by transforming a {L.MouseEvent} object.
   *
   * @method triggerMouseEvent_
   * @private
   * @param {string} eventName The event to fire on the MarkerCollection.
   * @param {L.MouseEvent} eventObj
   */
  MarkerCluster.prototype.triggerMouseEvent_ = function(eventName, eventObj) {
    var latLon = mapUtil.toAerisLatLon(eventObj.latlng);

    this.object_.trigger(eventName, latLon);
  };

  /**
   * @method destroy
   */
  MarkerCluster.prototype.destroy = function() {
    var markerViews = this.object_.models.
      map(function(marker) {
        return marker.getView();
      });
    var mapView = this.mapView_;

    AbstractStrategy.prototype.destroy.call(this);

    // Put each marker view back on the map.
    // --> we're destroying the clustering strategy,
    //    but we still want the markers to be rendered.
    if (mapView) {
      markerViews.forEach(function(view) {
        view.addTo(mapView);
      }, this);
    }
  };


  /**
   * @property SINGLE_CLUSTER_GROUP_NAME_
   * @constant
   * @type {string}
   * @private
   */
  MarkerCluster.SINGLE_CLUSTER_GROUPNAME_ = 'MARKERCLUSTERSTRATEGY_SINGLE_CLUSTER';


  /**
   * @property CLUSTER_CSS_CLASS_
   * @type {string}
   * @private
   */
  MarkerCluster.CLUSTER_CSS_CLASS_ = 'aeris-cluster aeris-leaflet';


  return MarkerCluster;
});
/**
 * @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
 */