Fork me on GitHub
Show:

File: ../src/maps/gmaps/layers/maptype/imagemaptype.js

define([
  'aeris/util',
  'aeris/maps/strategy/layers/maptype/imagemaptypeeventhub',
  'googlemaps!'
], function(_, mapTypeEventHub, gmaps) {
  /**
   * Similar to a {google.maps.ImageMapType}
   * but with some additional functionality.
   *
   * See https://developers.google.com/maps/documentation/javascript/reference#ImageMapTypeOptions
   *  for additional parameter options.
   *
   * @class aeris.maps.gmaps.layers.maptype.ImageMapType
   * @implements google.maps.MapType
   *
   * @param {Object} options
   * @param {number=} options.zIndex Optional.
   * @param {number=} options.opacity Optional.
   * @param {Function} options.getTileUrl Required.
   * @param {google.maps.Size} options.tileSize Required.
   *
   * @constructor
   */
  var ImageMapType = function(options) {
    _.defaults(options, {
      zIndex: 1,
      opacity: 1,
      tileSize: new gmaps.Size(256, 256)
    });

    _.extend(this, _.pick(options, [
      'alt',
      'maxZoom',
      'minZoom',
      'name',
      'projection',
      'radius',
      'tileSize'
    ]));


    /**
     * See {google.maps.ImageMapType}#getTileUrl
     * @type {Function}
     * @property getTileUrl_
     */
    this.getTileUrl_ = options.getTileUrl;


    /**
     * @type {Array.<HTMLElement>}
     * @private
     * @property divs_
     */
    this.divs_ = [];


    /**
     * Images created for each tile.
     *
     * @type {Array.<HTMLElement>}
     * @private
     * @property imgs_
     */
    this.imgs_ = [];

    this.setZIndex(options.zIndex);


    /**
     * @type {number}
     * @private
     * @property opacity_
     */
    this.opacity_ = options.opacity;


    /**
     * @property document_
     * @private
     * @type {HTMLElement}
     */
    this.document_ = null;

    // Keep of hash ([string]:boolean) of imgSrcs,
    // so we know which ones are loaded
    this.imageStatus_ = {};

    // I can't figure out
    // how to get zIndexes to work with gMaps.
    // I give up.
    setInterval(function() {
      this.setZIndex(this.zIndex_);
    }.bind(this), 500);

    // Update our zIndex
    // whenever other ImageMapTypes are
    // added to the DOM.
    // This seems to be necessary for
    // gmaps to save our zIndex state.
    mapTypeEventHub.on('init', function(imageMapType) {
      // Reset our zIndex.
      this.setZIndex(this.zIndex_);
    }, this);
  };

  /**
   * @override
   * @param {Object} coord
   * @param {number} zoom
   * @param {HTMLElement} ownerDocument
   * @return {HTMLElement}
   * @method getTile
   */
  ImageMapType.prototype.getTile = function(coord, zoom, ownerDocument) {
    var tileSrc, img, tileContainer;

    this.document_ = this.document_ || ownerDocument;

    coord = this.getNormalizedCoord_(coord, zoom);

    tileContainer = this.createTileContainer_(ownerDocument);

    if (coord) {
      tileSrc = this.getTileUrl_(coord, zoom);

      img = this.createTileImage_(ownerDocument, tileSrc);

      this.imageStatus_[tileSrc] = false;
      img.onload = _.bind(function() {
        // Mark the image as loaded
        this.imageStatus_[tileSrc] = true;

        // Fire a `load` event, if all images are loaded
        if (this.isLoaded()) {
          gmaps.event.trigger(this, 'load');
        }
      }, this);

      tileContainer.appendChild(img);

      this.imgs_.push(img);
    }

    this.setZIndex(this.zIndex_);

    // Keep track of the element,
    // so we can manipulate it later.
    this.divs_.push(tileContainer);


    // Google resets parent node styles
    // for all other map types when a new map
    // type is added.
    // This event will let other map types update their
    // styles to overcome google.
    mapTypeEventHub.trigger('init', this);

    return tileContainer;
  };

  ImageMapType.prototype.isLoaded = function() {
    return _.every(this.imageStatus_, Boolean);
  };

  /**
   * Normalize to keep coordinates in bounds of map tile range.
   *
   * See https://developers.google.com/maps/documentation/javascript/examples/maptype-image?csw=1
   *
   * @param {Object} coord
   * @param {number} zoom
   * @return {number}
   * @private
   * @method getNormalizedCoord_
   */
  ImageMapType.prototype.getNormalizedCoord_ = function(coord, zoom) {
    var y = coord.y;
    var x = coord.x;

    // tile range in one direction range is dependent on zoom level
    // 0 = 1 tile, 1 = 2 tiles, 2 = 4 tiles, 3 = 8 tiles, etc
    var tileRange = 1 << zoom;

    // don't repeat across y-axis (vertically)
    if (y < 0 || y >= tileRange) {
      return null;
    }

    // repeat across x-axis
    if (x < 0 || x >= tileRange) {
      x = (x % tileRange + tileRange) % tileRange;
    }

    return {
      x: x,
      y: y
    };
  };


  /**
   * @private
   * @param {HTMLElement} ownerDocument
   * @return {HTMLElement}
   * @method createTileContainer_
   */
  ImageMapType.prototype.createTileContainer_ = function(ownerDocument) {
    var div = ownerDocument.createElement('div');

    div.style.width = this.tileSize.width + 'px';
    div.style.height = this.tileSize.height + 'px';
    div.style.opacity = this.opacity_;

    return div;
  };


  /**
   * @private
   * @param {HTMLElement} ownerDocument
   * @param {string} imageSrc
   * @return {HTMLElement}
   * @method createTileImage_
   */
  ImageMapType.prototype.createTileImage_ = function(ownerDocument, imageSrc) {
    var img;

    if (!imageSrc) {
      throw new Error('Unable to create tile image: ' + imageSrc + ' is not a valid ' +
        'tile image source.');
    }

    img = ownerDocument.createElement('img');

    img.style.width = '100%';
    img.style.height = '100%';
    img.setAttribute('src', imageSrc);

    return img;
  };

  /**
   * Returns the parent node of the
   * entire map type. The siblings of this
   * node are other map types.
   *
   * Note that this method is fragile,
   * as it relies on an undocumented google maps
   * DOM structure. Whenever possible,
   * it's probably better to avoid using this method,
   * and directly interface with this.divs_, instead.
   *
   * @private
   * @return {?HTMLElement}
   * @method getParentNode_
   */
  ImageMapType.prototype.getParentNode_ = function() {
    var hasValidParentNode = this.parentNode_ && this.document_.contains(this.parentNode_);

    if (hasValidParentNode) {
      return this.parentNode_;
    }

    try {
      this.parentNode_ = this.divs_[0].parentNode.parentNode;
      this.setParentNodeZIndex_(this.zIndex_);
    }
    catch (e) {
      this.parentNode_ = null;
    }

    return this.parentNode_;
  };


  /**
   * @param {number} zIndex
   * @method setZIndex
   */
  ImageMapType.prototype.setZIndex = function(zIndex) {
    // Save our new zIndex state.
    this.zIndex_ = zIndex;

    // If we don't have a parent node yet,
    // wait until we have one, then update the zIndex
    if (!this.getParentNode_()) {
      var int = setInterval(function() {
        if (!this.getParentNode_()) {
          this.setParentNodeZIndex_(this.zIndex_);
          clearInterval(int);
        }
      }.bind(this), 100);
      return;
    }

    this.setParentNodeZIndex_(this.zIndex_);
  };


  /**
   * @private
   * @param {number} zIndex
   * @method setParentNodeZIndex_
   */
  ImageMapType.prototype.setParentNodeZIndex_ = function(zIndex) {
    var parentNode = this.getParentNode_();
    var parentNodeHasSameZIndex = parentNode && window.getComputedStyle(parentNode).zIndex === this.zIndex_;

    if (!parentNode || parentNodeHasSameZIndex) {
      return;
    }

    parentNode.style.zIndex = zIndex;
  };


  /**
   * @method releaseTile
   */
  ImageMapType.prototype.releaseTile = function(div) {
    var divIndex = this.divs_.indexOf(div);
    var imgIndex = this.imgs_.indexOf(img);
    var img = div.getElementsByTagName('img')[0];
    var imgSrc = img.getAttribute('src');
    delete this.imageStatus_[imgSrc];

    if (divIndex !== -1) {
      this.divs_.splice(divIndex, 1);
    }
    if (imgIndex !== -1) {
      this.imgs_.splice(imgIndex, 1);
    }
  };


  /**
   * @param {number} opacity
   * @method setOpacity
   */
  ImageMapType.prototype.setOpacity = function(opacity) {
    if (this.opacity_ === opacity) {
      return;
    }

    this.opacity_ = opacity;

    _.each(this.divs_, function(div) {
      if (window.getComputedStyle(div).opacity === this.opacity_) {
        return;
      }

      div.style.opacity = this.opacity_;
    }, this);
  };


  return ImageMapType;
});