Fork me on GitHub
Show:

File: ../src/maps/leaflet/map.js

define([
  'aeris/util',
  'aeris/maps/abstractstrategy',
  'aeris/maps/layers/osm',
  'aeris/maps/strategy/util',
  'leaflet'
], function(_, AbstractStrategy, OSMLayer, mapUtil, Leaflet) {
  /**
   * A map rendering strategy using Leaflet.
   *
   * @class aeris.maps.leaflet.Map
   * @extends aeris.maps.strategy.AbstractStrategy
   *
   * @constructor
   */
  var LeafletMapStrategy = function(aerisMap) {
    AbstractStrategy.call(this, aerisMap);

    /**
     * @property object_
     * @private
     * @type {aeris.maps.Map}
     * @override
     */
    /**
     * @property view_
     * @private
     * @type {L.Map}
     * @override
     */

      // We need to allow the map view to finish initializing
      // before asking a layer to be set on it.
    this.ensureBaseLayer_();


    this.updateObjectFromView_();
    this.updateLeafletMapPosition_();

    this.proxyLeafletMapEvents_();
    this.bindToLeafletMapState_();

    this.updateSizeWhenCanasAddedToDOM_();
  };
  _.inherits(LeafletMapStrategy, AbstractStrategy);


  /**
   * @method createView_
   * @private
   * @override
   *
   * @return {L.Map}
   */
  LeafletMapStrategy.prototype.createView_ = function() {
    var map;
    var el = this.object_.getElement();

    // Use predefined map view
    if (el instanceof Leaflet.Map) {
      return el;
    }

    map = new Leaflet.Map(el, {
      center: mapUtil.toLeafletLatLng(this.object_.getCenter()),
      zoom: this.object_.getZoom(),
      scrollWheelZoom: this.object_.get('scrollZoom')
    });

    return map;
  };


  /**
   * Sets a base layer, if none is already set.
   *
   * @method ensureBaseLayer_
   * @private
   */
  LeafletMapStrategy.prototype.ensureBaseLayer_ = function() {
    var baseLayer = this.object_.getBaseLayer();
    if (!baseLayer) {
      baseLayer = new LeafletMapStrategy.DEFAULT_BASE_LAYER_TYPE_();
      this.object_.setBaseLayer(baseLayer);
    }

    if (!this.getLayerCount_()) {
      this.renderBaseLayer_(baseLayer);
    }
  };


  /**
   * @property DEFAULT_BASE_LAYER_TYPE_
   * @static
   * @type {function():aeris.maps.layers.Layer}
   * @private
   */
  LeafletMapStrategy.DEFAULT_BASE_LAYER_TYPE_ = OSMLayer;


  /**
   * @method getLayerCount_
   * @private
   */
  LeafletMapStrategy.prototype.getLayerCount_ = function() {
    var count = 0;

    // We have indirect access to the
    // maps layers via the #eachLayer method.
    this.view_.eachLayer(function() {
      count++;
    });

    return count;
  };


  /**
   * @method updateLeafletMapPosition_
   * @private
   */
  LeafletMapStrategy.prototype.updateLeafletMapPosition_ = function() {
    this.view_.setView(this.object_.getCenter(), this.object_.getZoom());
  };


  /**
   * @method proxyLeafletMapEvents_
   * @private
   */
  LeafletMapStrategy.prototype.proxyLeafletMapEvents_ = function() {
    this.proxyLeafletMouseEvent_('click');
    this.proxyLeafletMouseEvent_('dblclick');

    this.view_.addEventListener({
      load: function() {
        this.object_.trigger('load');
      }.bind(this)
    });
  };

  /**
   * @param {string} leafletTopic
   * @param {string=} opt_aerisTopic
   * @private
   */
  LeafletMapStrategy.prototype.proxyLeafletMouseEvent_ = function(leafletTopic, opt_aerisTopic) {
    var aerisTopic = opt_aerisTopic || leafletTopic;

    this.view_.addEventListener(leafletTopic, function(evt) {
      var latLon = mapUtil.toAerisLatLon(evt.latlng);
      this.object_.trigger(aerisTopic, latLon);
    }.bind(this));
  };


  /**
   * @method bindToLeafletMapState_
   * @private
   */
  LeafletMapStrategy.prototype.bindToLeafletMapState_ = function() {
    this.view_.addEventListener({
      moveend: this.updateObjectFromView_.bind(this)
    });

    this.listenTo(this.object_, {
      'change:center change:zoom': this.updateLeafletMapPosition_,
      'change:baseLayer': this.updateBaseLayer_,
      'updateSize': this.updateSize_
    });
  };


  /**
   * @method updateObjectFromView_
   * @private
   */
  LeafletMapStrategy.prototype.updateObjectFromView_ = function() {
    this.object_.set({
      center: mapUtil.toAerisLatLon(this.view_.getCenter()),
      bounds: mapUtil.toAerisBounds(this.view_.getBounds()),
      zoom: this.view_.getZoom(),
      scrollZoom: this.view_.options.scrollWheelZoom
    }, { validate: true });
  };


  /**
   * @method updateBaseLayer_
   * @private
   */
  LeafletMapStrategy.prototype.updateBaseLayer_ = function() {
    var baseLayer = this.object_.getBaseLayer();
    var previousBaseLayer = this.object_.previousAttributes().baseLayer;
    var isSameBaseLayer = baseLayer === previousBaseLayer;

    if (isSameBaseLayer) {
      return;
    }
    if (previousBaseLayer) {
      this.view_.removeLayer(previousBaseLayer.getView());
      previousBaseLayer.set('map', null, { silent: true });
    }

    this.renderBaseLayer_(baseLayer);
  };


  /**
   * @method renderBaseLayer_
   * @private
   * @param {aeris.maps.layers.Layer} baseLayer
   */
  LeafletMapStrategy.prototype.renderBaseLayer_ = function(baseLayer) {
    this.view_.addLayer(baseLayer.getView(), true);

    // Manually update map attribute, without the base layer
    // trying to update the view itself.
    baseLayer.set('map', this.object_, { silent: true });
  };


  /**
   * @method fitToBounds
   * @param {aeris.maps.Bounds} bounds
   */
  LeafletMapStrategy.prototype.fitToBounds = function(bounds) {
    this.view_.fitBounds(mapUtil.toLeafletBounds(bounds));
  };


  /**
   * @method updateSizeWhenCanasAddedToDOM_
   * @private
   */
  LeafletMapStrategy.prototype.updateSizeWhenCanasAddedToDOM_ = function() {
    var el = this.object_.mapEl_;
    var isElDrawn = function() {
      return el.offsetWidth !== 0 && el.offsetHeight !== 0;
    };

    function pollUntil(predicate, cb, pollInterval) {
      var pollTimer;
      var predicateChecker = function() {
        if (predicate()) {
          cb();
          root.clearInterval(pollTimer);
        }
      };
      pollTimer = root.setInterval(predicateChecker, pollInterval);
      predicateChecker();
    }

    pollUntil(isElDrawn, this.updateSize_.bind(this), 100);
  };

  /**
   * @method updateSize_
   * @private
   */
  LeafletMapStrategy.prototype.updateSize_ = function() {
    this.getView().invalidateSize();
  };


  var root = this;

  return LeafletMapStrategy;
});