Fork me on GitHub
Show:

File: ../src/maps/layers/abstracttile.js

define([
  'aeris/util',
  'aeris/promise',
  'aeris/events',
  'aeris/errors/unimplementedpropertyerror',
  'aeris/errors/validationerror',
  'aeris/maps/layers/errors/layerloadingerror',
  'aeris/maps/layers/animationlayer',
  'aeris/maps/strategy/layers/tile'
], function(_, Promise, Events, UnimplementedPropertyError, ValidationError, LayerLoadingError, AnimationLayer, TileStrategy) {
  /**
   * Representation of image tile layer. Tile layers are
   * expected to pull in tile images from an API.
   *
   *
   * @constructor
   * @class aeris.maps.layers.AbstractTile
   * @extends aeris.maps.layers.AnimationLayer
   */
  var AbstractTile = function(opt_attrs, opt_options) {
    /**
     * Fires when tile images are loaded.
     *
     * @event load
     */
    /**
     * Firest when tile images must
     * be re-loaded (eg. if the map bounds change)
     *
     * @event load:reset
     */

    var options = _.extend({
      strategy: TileStrategy,
      validate: true
    }, opt_options);


    var attrs = _.extend({
      /**
       * An array of subdomains to use for load balancing tile requests.
       *
       * @attribute subdomains
       * @type {Array.<string>}
       * @abstract
       */
      subdomains: [],


      /**
       * The name of the tile layer.
       *
       * The value of the name can be anything,
       * though some map views will display this name
       * in layer-select controls.
       *
       * @attribute name
       * @type {string}
       * @abstract
       */
      name: undefined,


      /**
       * The server used for requesting tiles. The server will be interpolated by replacing
       * special variables with calculated values. Special variables should be
       * wrapped with '{' and '}'
       *
       * * {d} - a randomly selected subdomain
       *
       * @attribute server
       * @type {string}
       * @abstract
       */
      server: undefined,


      /**
       * The minimum zoom level provided by the tile renderer.
       *
       * @attribute minZoom
       * @type {number}
       * @default 0
       */
      minZoom: 0,


      /**
       * The maximum zoom level provided by the tile renderer.
       *
       * @attribute maxZoom
       * @type {number}
       * @default 22
       */
      maxZoom: 22,


      /**
       * @attribute opacity
       * @type {number} Between 0 and 1.0
       */
      opacity: 1.0,


      /**
       * @attribute zIndex
       * @type {number}
       */
      zIndex: 1
    }, opt_attrs);

    this.listenTo(this, {
      'load': function() {
        this.loaded_ = true;
      },
      'load:reset': function() {
        this.loaded_ = false;
      }
    });

    AnimationLayer.call(this, attrs, options);
  };

  _.inherits(AbstractTile, AnimationLayer);


  /**
   * @method validate
   */
  AbstractTile.prototype.validate = function(attrs) {
    if (!_.isString(attrs.server)) {
      return new ValidationError('server', 'not a valid string');
    }
    if (
      !_.isNumber(attrs.opacity) ||
      attrs.opacity > 1 ||
      attrs.opacity < 0
    ) {
      return new ValidationError('opacity', 'must be a number between 0 and 1');
    }

    return AnimationLayer.prototype.validate.apply(this, arguments);
  };


  /**
   * Returns the url for requesting tiles. The url will be interpolated by replacing
   * special variables with calculated values. Special variables should be
   * wrapped in brackets.
   *
   * * d - a randomly selected subdomain
   * * z - the calculated zoom factor
   * * x - the tile's starting x coordinate
   * * y - the tile's starting y coordinate
   *
   * ex. http://{d}.tileserver.net/{z}/{x}/{y}.png
   *
   * @type {string}
   * @return {string} default url for tile image.
   * @method getUrl
   */
  AbstractTile.prototype.getUrl = _.abstractMethod;


  /**
   * @return {string} A random subdomain for the tile server.
   * @method getRandomSubdomain
   */
  AbstractTile.prototype.getRandomSubdomain = function() {
    var index = Math.floor(Math.random() * this.get('subdomains').length);
    return this.get('subdomains')[index];
  };


  /**
   * Implemented map specific zoom factor calculation.
   *
   * @param {number} zoom the map's current zoom level.
   * @return {number}
   * @method zoomFactor
   */
  AbstractTile.prototype.zoomFactor = function(zoom) {
    return zoom;
  };


  /**
   * Sets the opacity of the tile layer.
   *
   * @param {number} opacity Between 0 and 1.
   * @method setOpacity
   */
  AbstractTile.prototype.setOpacity = function(opacity) {
    this.set('opacity', opacity, { validate: true });
  };

  /**
   * @method getOpacity
   * @return {number}
   */
  AbstractTile.prototype.getOpacity = function() {
    return this.get('opacity');
  };


  /**
   * Sets the zIndex of a tile layer.
   *
   * @type {*}
   * @method setZIndex
   */
  AbstractTile.prototype.setZIndex = function(zIndex) {
    this.set('zIndex', zIndex, { validate: true });
  };


  /**
   * @method getZIndex
   * @return {number}
   */
  AbstractTile.prototype.getZIndex = function() {
    return this.get('zIndex');
  };


  /**
   * @return {Boolean} True, if tile images have finished loading.
   * @method isLoaded
   */
  AbstractTile.prototype.isLoaded = function() {
    return !!this.loaded_;
  };


  /**
   * Preloads the tile layer images.
   *
   * @method preload
   * @param {aeris.maps.Map} map
   *        The layer will be temporarily set to this
   *        map, in order to trigger it's tile images
   *        to start loading.
   */
  AbstractTile.prototype.preload = function(map) {
    if (this.isPreloading_ || this.isLoaded()) {
      return Promise.resolve();
    }
    this.isPreloading_ = true;

    var promiseToLoad = new Promise();
    var attrs_orig = this.pick(['opacity']);
    var attrListener = new Events();

    // We're already loaded
    // -- resolve immediately.
    if (this.isLoaded()) {
      promiseToLoad.resolve();
      return promiseToLoad;
    }
    // We don't have a map to use,
    // so that's all
    if (!map) {
      this.isPreloading_ = false;
      promiseToLoad.reject(new LayerLoadingError('Unable to preload Tile: no map has been specified.'));
      return promiseToLoad;
    }

    this.listenToOnce(this, 'load', function() {
      attrListener.stopListening();
      attrListener.off();

      if (this.hasMap()) {
        this.strategy_.setMap(this.getMap());
      }
      else {
        this.strategy_.remove();
      }

      this.set(attrs_orig);
      this.isPreloading_ = false;
      promiseToLoad.resolve();
    });

    this.set({
      // Temporarily set to 0 opacity, so we don't see
      // the layer being added to the map
      opacity: 0
    });
    // Trigger the layer to load, by setting its
    // view to a map.
    this.strategy_.setMap(map);

    // Listen for any changes made during preloading,
    // so we can make sure to reset our object to the expected state.
    attrListener.listenTo(this, {
      'change:opacity': function(obj, opacity) {
        attrs_orig.opacity = opacity;
      }
    });

    return promiseToLoad;
  };


  return AbstractTile;

});