tile.js

var $ = require('jquery');

/**
 * @typedef {object} geo.tile.spec
 *
 * @property {object} index The global position of the tile.
 * @property {number} index.x The x-coordinate (usually the column number).
 * @property {number} index.y The y-coordinate (usually the row number).
 * @property {object} size The size of each tile.
 * @property {number} size.x Width (usually in pixels).
 * @property {number} size.y Height (usually in pixels).
 * @property {object|string} url A url or jQuery ajax config object.
 * @property {object} [overlap] The size of overlap with neighboring tiles.
 * @property {number} overlap.x=0
 * @property {number} overlap.y=0
 */

/**
 * This class defines the raw interface for a "tile" on a map.  A tile is
 * defined as a quadrilateral section of a map.  The base implementation is
 * independent of the actual content of the tile, but assumes that the content
 * is loaded asynchronously via a url.  The tile object has a promise-like
 * interface.
 * @example
 * tile.then(function (data) {...}).catch(function (data) {...});
 *
 * @class
 * @alias geo.tile
 * @param {geo.tile.spec} spec The tile specification.
 * @returns {geo.tile}
 */
var tile = function (spec) {
  if (!(this instanceof tile)) {
    return new tile(spec);
  }

  this._index = spec.index;
  this._size = spec.size;
  this._overlap = spec.overlap || {x: 0, y: 0};
  this._wrap = spec.wrap || {x: 1, y: 1};
  this._url = spec.url;
  this._fetched = false;
  this._queue = spec.queue || null;

  /**
   * Return the index coordinates.  Read only.
   *
   * @property {object} index The tile index.
   * @property {number} index.x The tile x index.
   * @property {number} index.y The tile y index.
   * @property {number} [index.level] The tile level index.
   * @property {number} [index.reference] The tile reference index.
   * @name geo.tile#index
   */
  Object.defineProperty(this, 'index', {
    get:
      function () { return this._index; }
  });

  /**
   * Return the tile size.  Read only.
   *
   * @property {object} size The tile size.
   * @property {number} size.x The tile width.
   * @property {number} size.y The tile height.
   * @name geo.tile#size
   */
  Object.defineProperty(this, 'size', {
    get: function () { return this._size; }
  });

  /**
   * Return the tile overlap.  Read only.
   *
   * @property {object} overlap The tile overlap.
   * @property {number} overlap.x The tile x overlap.
   * @property {number} overlap.y The tile y overlap.
   * @name geo.tile#overlap
   */
  Object.defineProperty(this, 'overlap', {
    get: function () { return this._overlap; }
  });

  /**
   * Initiate the ajax request and add a promise interface to the tile
   * object.  This method exists to allow derived classes the ability to
   * override how the tile is obtained.  For example, imageTile uses an
   * Image element rather than $.get.
   *
   * @returns {this}
   */
  this.fetch = function () {
    if (!this._fetched) {
      $.get(this._url).done(function () {
        this._fetched = true;
      }.bind(this)).promise(this);
    }
    return this;
  };

  /**
   * Return whether this tile has been fetched already.
   *
   * @returns {boolean} True if the tile has been fetched.
   */
  this.fetched = function () {
    return this._fetched;
  };

  /**
   * Add a method to be called with the data when the ajax request is
   * successfully resolved.
   *
   * @param {function?} onSuccess The success handler.
   * @param {function?} onFailure The failure handler.
   * @returns {this}
   */
  this.then = function (onSuccess, onFailure) {
    // both fetch and _queueAdd can replace the current then method
    if (!this.fetched() && this._queue && this._queue.add && (!this.state ||
        this.state() === 'pending')) {
      this._queue.add(this, this.fetch);
    } else {
      this.fetch();
    }
    // Call then on the new promise
    if (this.done && this.fail) {
      this.done(onSuccess).fail(onFailure);
    } else {
      this.then(onSuccess, onFailure);
    }
    return this;
  };

  /**
   * Add a method to be called with the data when the ajax fails.
   *
   * @param {function} method The rejection handler.
   * @returns {this}
   */
  this.catch = function (method) {
    this.then(undefined, method);
    return this;
  };

  /**
   * Return a unique string representation of the given tile usable as a hash
   * key.  Possibly extend later to include url information to make caches
   * aware of the tile source.
   *
   * @returns {string}
   */
  this.toString = function () {
    return [this._index.level || 0, this._index.y, this._index.x, this._index.reference || 0].join('_');
  };

  /**
   * Return the bounds of the tile given an index offset and a translation.
   *
   * @param {object} index The tile index containing (0, 0).
   * @param {object} shift The coordinates of (0, 0) inside the tile.
   * @returns {object} An object with `left`, `top`, `right`, `bottom`.
   */
  this.bounds = function (index, shift) {
    var left, right, bottom, top;
    left = this.size.x * (this.index.x - index.x) - this.overlap.x - shift.x;
    right = left + this.size.x + this.overlap.x * 2;
    top = this.size.y * (this.index.y - index.y) - this.overlap.y - shift.y;
    bottom = top + this.size.y + this.overlap.y * 2;
    if (this.overlap.x && this.index.x === index.x) {
      left += this.overlap.x;
    }
    if (this.overlap.y && this.index.y === index.y) {
      top += this.overlap.y;
    }
    return {
      left: left,
      right: right,
      bottom: bottom,
      top: top
    };
  };

  /**
   * Computes the global coordinates of the bottom edge.
   * @property {number} bottom The global coordinates of the bottom edge.
   * @name geo.tile#bottom
   */
  Object.defineProperty(this, 'bottom', {
    get: function () {
      return this.size.y * (this.index.y + 1) + this.overlap.y;
    }
  });

  /**
   * Computes the global coordinates of the top edge.
   * @property {number} top The global coordinates of the top edge.
   * @name geo.tile#top
   */
  Object.defineProperty(this, 'top', {
    get: function () {
      return this.size.y * this.index.y - (this.index.y ? this.overlap.y : 0);
    }
  });

  /**
   * Computes the global coordinates of the left edge.
   * @property {number} left The global coordinates of the left edge.
   * @name geo.tile#left
   */
  Object.defineProperty(this, 'left', {
    get: function () {
      return this.size.x * this.index.x - (this.index.x ? this.overlap.x : 0);
    }
  });

  /**
   * Computes the global coordinates of the right edge.
   * @property {number} right The global coordinates of the right edge.
   * @name geo.tile#right
   */
  Object.defineProperty(this, 'right', {
    get: function () {
      return this.size.x * (this.index.x + 1) + this.overlap.x;
    }
  });

  /**
   * Set the opacity of the tile to 0 and gradually fade in over the given
   * number of milliseconds.  This is just a delay.
   *
   * @param {number} duration The duration of the animation in ms.
   * @returns {this}
   */
  this.fadeIn = function (duration) {
    $.noop(duration);
    return this;
  };
};

module.exports = tile;