# Source: src/ol/style/iconstyle.js

    goog.provide('ol.style.Icon');
    goog.provide('ol.style.IconAnchorUnits');
    goog.provide('ol.style.IconImageCache');
    goog.provide('ol.style.IconOrigin');

    goog.require('goog.array');
    goog.require('goog.asserts');
    goog.require('goog.events');
    goog.require('goog.events.EventTarget');
    goog.require('goog.events.EventType');
    goog.require('ol.dom');
    goog.require('ol.style.Image');
    goog.require('ol.style.ImageState');

    /**
     * @enum {string}
     */
    ol.style.IconAnchorUnits = {
      FRACTION: 'fraction',
      PIXELS: 'pixels'
    };

    /**
     * @enum {string}
     */
    ol.style.IconOrigin = {
      BOTTOM_LEFT: 'bottom-left',
      BOTTOM_RIGHT: 'bottom-right',
      TOP_LEFT: 'top-left',
      TOP_RIGHT: 'top-right'
    };

    /**
     * @classdesc
     * Set icon style for vector features.
     *
     * @constructor
     * @param {olx.style.IconOptions=} opt_options Options.
     * @extends {ol.style.Image}
     * @todo api
     */
    ol.style.Icon = function(opt_options) {

      var options = goog.isDef(opt_options) ? opt_options : {};

      /**
       * @private
       * @type {Array.<number>}
       */
      this.anchor_ = goog.isDef(options.anchor) ? options.anchor : [0.5, 0.5];

      /**
       * @private
       * @type {Array.<number>}
       */
      this.normalizedAnchor_ = null;

      /**
       * @private
       * @type {ol.style.IconOrigin}
       */
      this.anchorOrigin_ = goog.isDef(options.anchorOrigin) ?
          options.anchorOrigin : ol.style.IconOrigin.TOP_LEFT;

      /**
       * @private
       * @type {ol.style.IconAnchorUnits}
       */
      this.anchorXUnits_ = goog.isDef(options.anchorXUnits) ?
          options.anchorXUnits : ol.style.IconAnchorUnits.FRACTION;

      /**
       * @private
       * @type {ol.style.IconAnchorUnits}
       */
      this.anchorYUnits_ = goog.isDef(options.anchorYUnits) ?
          options.anchorYUnits : ol.style.IconAnchorUnits.FRACTION;

      /**
       * @type {?string}
       */
      var crossOrigin =
          goog.isDef(options.crossOrigin) ? options.crossOrigin : null;

      /**
       * @type {Image}
       */
      var image = goog.isDef(options.img) ? options.img : null;

      /**
       * @type {string|undefined}
       */
      var src = options.src;

      if ((!goog.isDef(src) || src.length === 0) && !goog.isNull(image)) {
        src = image.src;
      }
      goog.asserts.assert(goog.isDef(src) && src.length > 0);

      /**
       * @type {ol.style.ImageState}
       */
      var imageState = goog.isDef(options.src) ?
          ol.style.ImageState.IDLE : ol.style.ImageState.LOADED;

      /**
       * @private
       * @type {ol.style.IconImage_}
       */
      this.iconImage_ = ol.style.IconImage_.get(
          image, src, crossOrigin, imageState);

      /**
       * @private
       * @type {Array.<number>}
       */
      this.offset_ = goog.isDef(options.offset) ? options.offset : [0, 0];

      /**
       * @private
       * @type {ol.style.IconOrigin}
       */
      this.offsetOrigin_ = goog.isDef(options.offsetOrigin) ?
          options.offsetOrigin : ol.style.IconOrigin.TOP_LEFT;

      /**
       * @private
       * @type {Array.<number>}
       */
      this.origin_ = null;

      /**
       * @private
       * @type {ol.Size}
       */
      this.size_ = goog.isDef(options.size) ? options.size : null;

      /**
       * @type {number}
       */
      var opacity = goog.isDef(options.opacity) ? options.opacity : 1;

      /**
       * @type {boolean}
       */
      var rotateWithView = goog.isDef(options.rotateWithView) ?
          options.rotateWithView : false;

      /**
       * @type {number}
       */
      var rotation = goog.isDef(options.rotation) ? options.rotation : 0;

      /**
       * @type {number}
       */
      var scale = goog.isDef(options.scale) ? options.scale : 1;

      /**
       * @type {boolean}
       */
      var snapToPixel = goog.isDef(options.snapToPixel) ?
          options.snapToPixel : true;

      goog.base(this, {
        opacity: opacity,
        rotation: rotation,
        scale: scale,
        snapToPixel: snapToPixel,
        rotateWithView: rotateWithView
      });

    };
    goog.inherits(ol.style.Icon, ol.style.Image);

    /**
     * @inheritDoc
     * @todo api
     */
    ol.style.Icon.prototype.getAnchor = function() {
      if (!goog.isNull(this.normalizedAnchor_)) {
        return this.normalizedAnchor_;
      }
      var anchor = this.anchor_;
      var size = this.getSize();
      if (this.anchorXUnits_ == ol.style.IconAnchorUnits.FRACTION ||
          this.anchorYUnits_ == ol.style.IconAnchorUnits.FRACTION) {
        if (goog.isNull(size)) {
          return null;
        }
        anchor = this.anchor_.slice();
        if (this.anchorXUnits_ == ol.style.IconAnchorUnits.FRACTION) {
          anchor[0] *= size[0];
        }
        if (this.anchorYUnits_ == ol.style.IconAnchorUnits.FRACTION) {
          anchor[1] *= size[1];
        }
      }

      if (this.anchorOrigin_ != ol.style.IconOrigin.TOP_LEFT) {
        if (goog.isNull(size)) {
          return null;
        }
        if (anchor === this.anchor_) {
          anchor = this.anchor_.slice();
        }
        if (this.anchorOrigin_ == ol.style.IconOrigin.TOP_RIGHT ||
            this.anchorOrigin_ == ol.style.IconOrigin.BOTTOM_RIGHT) {
          anchor[0] = -anchor[0] + size[0];
        }
        if (this.anchorOrigin_ == ol.style.IconOrigin.BOTTOM_LEFT ||
            this.anchorOrigin_ == ol.style.IconOrigin.BOTTOM_RIGHT) {
          anchor[1] = -anchor[1] + size[1];
        }
      }
      this.normalizedAnchor_ = anchor;
      return this.normalizedAnchor_;
    };

    /**
     * @inheritDoc
     * @todo api
     */
    ol.style.Icon.prototype.getImage = function(pixelRatio) {
      return this.iconImage_.getImage(pixelRatio);
    };

    /**
     * @inheritDoc
     */
    ol.style.Icon.prototype.getImageState = function() {
      return this.iconImage_.getImageState();
    };

    /**
     * @inheritDoc
     */
    ol.style.Icon.prototype.getHitDetectionImage = function(pixelRatio) {
      return this.iconImage_.getHitDetectionImage(pixelRatio);
    };

    /**
     * @inheritDoc
     * @todo api
     */
    ol.style.Icon.prototype.getOrigin = function() {
      if (!goog.isNull(this.origin_)) {
        return this.origin_;
      }
      var offset = this.offset_;

      if (this.offsetOrigin_ != ol.style.IconOrigin.TOP_LEFT) {
        var size = this.getSize();
        var iconImageSize = this.iconImage_.getSize();
        if (goog.isNull(size) || goog.isNull(iconImageSize)) {
          return null;
        }
        offset = offset.slice();
        if (this.offsetOrigin_ == ol.style.IconOrigin.TOP_RIGHT ||
            this.offsetOrigin_ == ol.style.IconOrigin.BOTTOM_RIGHT) {
          offset[0] = iconImageSize[0] - size[0] - offset[0];
        }
        if (this.offsetOrigin_ == ol.style.IconOrigin.BOTTOM_LEFT ||
            this.offsetOrigin_ == ol.style.IconOrigin.BOTTOM_RIGHT) {
          offset[1] = iconImageSize[1] - size[1] - offset[1];
        }
      }
      this.origin_ = offset;
      return this.origin_;
    };

    /**
     * @return {string|undefined} Image src.
     * @todo api
     */
    ol.style.Icon.prototype.getSrc = function() {
      return this.iconImage_.getSrc();
    };

    /**
     * @inheritDoc
     * @todo api
     */
    ol.style.Icon.prototype.getSize = function() {
      return goog.isNull(this.size_) ? this.iconImage_.getSize() : this.size_;
    };

    /**
     * @inheritDoc
     */
    ol.style.Icon.prototype.listenImageChange = function(listener, thisArg) {
      return goog.events.listen(this.iconImage_, goog.events.EventType.CHANGE,
          listener, false, thisArg);
    };

    /**
     * Load not yet loaded URI.
     */
    ol.style.Icon.prototype.load = function() {
      this.iconImage_.load();
    };

    /**
     * @inheritDoc
     */
    ol.style.Icon.prototype.unlistenImageChange = function(listener, thisArg) {
      goog.events.unlisten(this.iconImage_, goog.events.EventType.CHANGE,
          listener, false, thisArg);
    };

    /**
     * @constructor
     * @param {Image} image Image.
     * @param {string|undefined} src Src.
     * @param {?string} crossOrigin Cross origin.
     * @param {ol.style.ImageState} imageState Image state.
     * @extends {goog.events.EventTarget}
     * @private
     */
    ol.style.IconImage_ = function(image, src, crossOrigin, imageState) {

      goog.base(this);

      /**
       * @private
       * @type {Image|HTMLCanvasElement}
       */
      this.hitDetectionImage_ = null;

      /**
       * @private
       * @type {Image}
       */
      this.image_ = goog.isNull(image) ? new Image() : image;

      if (!goog.isNull(crossOrigin)) {
        this.image_.crossOrigin = crossOrigin;
      }

      /**
       * @private
       * @type {Array.<number>}
       */
      this.imageListenerKeys_ = null;

      /**
       * @private
       * @type {ol.style.ImageState}
       */
      this.imageState_ = imageState;

      /**
       * @private
       * @type {ol.Size}
       */
      this.size_ = null;

      /**
       * @private
       * @type {string|undefined}
       */
      this.src_ = src;

      /**
       * @private
       * @type {boolean}
       */
      this.tainting_ = false;

    };
    goog.inherits(ol.style.IconImage_, goog.events.EventTarget);

    /**
     * @param {Image} image Image.
     * @param {string} src Src.
     * @param {?string} crossOrigin Cross origin.
     * @param {ol.style.ImageState} imageState Image state.
     * @return {ol.style.IconImage_} Icon image.
     */
    ol.style.IconImage_.get = function(image, src, crossOrigin, imageState) {
      var iconImageCache = ol.style.IconImageCache.getInstance();
      var iconImage = iconImageCache.get(src, crossOrigin);
      if (goog.isNull(iconImage)) {
        iconImage = new ol.style.IconImage_(image, src, crossOrigin, imageState);
        iconImageCache.set(src, crossOrigin, iconImage);
      }
      return iconImage;
    };

    /**
     * @private
     */
    ol.style.IconImage_.prototype.determineTainting_ = function() {
      var context = ol.dom.createCanvasContext2D(1, 1);
      context.drawImage(this.image_, 0, 0);
      try {
        context.getImageData(0, 0, 1, 1);
      } catch (e) {
        this.tainting_ = true;
      }
    };

    /**
     * @private
     */
    ol.style.IconImage_.prototype.dispatchChangeEvent_ = function() {
      this.dispatchEvent(goog.events.EventType.CHANGE);
    };

    /**
     * @private
     */
    ol.style.IconImage_.prototype.handleImageError_ = function() {
      this.imageState_ = ol.style.ImageState.ERROR;
      this.unlistenImage_();
      this.dispatchChangeEvent_();
    };

    /**
     * @private
     */
    ol.style.IconImage_.prototype.handleImageLoad_ = function() {
      this.imageState_ = ol.style.ImageState.LOADED;
      this.size_ = [this.image_.width, this.image_.height];
      this.unlistenImage_();
      this.determineTainting_();
      this.dispatchChangeEvent_();
    };

    /**
     * @param {number} pixelRatio Pixel ratio.
     * @return {Image} Image element.
     */
    ol.style.IconImage_.prototype.getImage = function(pixelRatio) {
      return this.image_;
    };

    /**
     * @return {ol.style.ImageState} Image state.
     */
    ol.style.IconImage_.prototype.getImageState = function() {
      return this.imageState_;
    };

    /**
     * @param {number} pixelRatio Pixel ratio.
     * @return {Image|HTMLCanvasElement} Image element.
     */
    ol.style.IconImage_.prototype.getHitDetectionImage = function(pixelRatio) {
      if (goog.isNull(this.hitDetectionImage_)) {
        if (this.tainting_) {
          var width = this.size_[0];
          var height = this.size_[1];
          var context = ol.dom.createCanvasContext2D(width, height);
          context.fillRect(0, 0, width, height);
          this.hitDetectionImage_ = context.canvas;
        } else {
          this.hitDetectionImage_ = this.image_;
        }
      }
      return this.hitDetectionImage_;
    };

    /**
     * @return {ol.Size} Image size.
     */
    ol.style.IconImage_.prototype.getSize = function() {
      return this.size_;
    };

    /**
     * @return {string|undefined} Image src.
     */
    ol.style.IconImage_.prototype.getSrc = function() {
      return this.src_;
    };

    /**
     * Load not yet loaded URI.
     */
    ol.style.IconImage_.prototype.load = function() {
      if (this.imageState_ == ol.style.ImageState.IDLE) {
        goog.asserts.assert(goog.isDef(this.src_));
        goog.asserts.assert(goog.isNull(this.imageListenerKeys_));
        this.imageState_ = ol.style.ImageState.LOADING;
        this.imageListenerKeys_ = [
          goog.events.listenOnce(this.image_, goog.events.EventType.ERROR,
              this.handleImageError_, false, this),
          goog.events.listenOnce(this.image_, goog.events.EventType.LOAD,
              this.handleImageLoad_, false, this)
        ];
        try {
          this.image_.src = this.src_;
        } catch (e) {
          this.handleImageError_();
        }
      }
    };

    /**
     * Discards event handlers which listen for load completion or errors.
     *
     * @private
     */
    ol.style.IconImage_.prototype.unlistenImage_ = function() {
      goog.asserts.assert(!goog.isNull(this.imageListenerKeys_));
      goog.array.forEach(this.imageListenerKeys_, goog.events.unlistenByKey);
      this.imageListenerKeys_ = null;
    };

    /**
     * @constructor
     */
    ol.style.IconImageCache = function() {

      /**
       * @type {Object.<string, ol.style.IconImage_>}
       * @private
       */
      this.cache_ = {};

      /**
       * @type {number}
       * @private
       */
      this.cacheSize_ = 0;

      /**
       * @const
       * @type {number}
       * @private
       */
      this.maxCacheSize_ = 32;
    };
    goog.addSingletonGetter(ol.style.IconImageCache);

    /**
     * @param {string} src Src.
     * @param {?string} crossOrigin Cross origin.
     * @return {string} Cache key.
     */
    ol.style.IconImageCache.getKey = function(src, crossOrigin) {
      goog.asserts.assert(goog.isDef(crossOrigin));
      return crossOrigin + ':' + src;
    };

    /**
     * FIXME empty description for jsdoc
     */
    ol.style.IconImageCache.prototype.clear = function() {
      this.cache_ = {};
      this.cacheSize_ = 0;
    };

    /**
     * FIXME empty description for jsdoc
     */
    ol.style.IconImageCache.prototype.expire = function() {
      if (this.cacheSize_ > this.maxCacheSize_) {
        var i = 0;
        var key, iconImage;
        for (key in this.cache_) {
          iconImage = this.cache_[key];
          if ((i++ & 3) === 0 && !goog.events.hasListener(iconImage)) {
            delete this.cache_[key];
            --this.cacheSize_;
          }
        }
      }
    };

    /**
     * @param {string} src Src.
     * @param {?string} crossOrigin Cross origin.
     * @return {ol.style.IconImage_} Icon image.
     */
    ol.style.IconImageCache.prototype.get = function(src, crossOrigin) {
      var key = ol.style.IconImageCache.getKey(src, crossOrigin);
      return key in this.cache_ ? this.cache_[key] : null;
    };

    /**
     * @param {string} src Src.
     * @param {?string} crossOrigin Cross origin.
     * @param {ol.style.IconImage_} iconImage Icon image.
     */
    ol.style.IconImageCache.prototype.set = function(src, crossOrigin, iconImage) {
      var key = ol.style.IconImageCache.getKey(src, crossOrigin);
      this.cache_[key] = iconImage;
      ++this.cacheSize_;
    };