Source: style/styleproportional.js

goog.provide('M.style.Proportional');

goog.require('M.Style');
goog.require('M.style.Point');

/**
 * @namespace M.style.Proportional
 */
(function() {


  /**
   * @classdesc
   * Main constructor of the class. Creates a style Proportional
   * with parameters specified by the user
   *
   * @constructor
   * @extends {M.Style}
   * @param {String}
   * @param{number}
   * @param{number}
   * @param {M.style.Point}
   * @param {object}
   * @api stable
   */
  M.style.Proportional = (function(attributeName, minRadius, maxRadius, style, proportionalFunction, options = {}) {
    if (M.utils.isNullOrEmpty(attributeName)) {
      M.exception("No se ha especificado el nombre del atributo.");
    }

    /**
     * TODO
     * @public
     * @type {String}
     * @api stable
     * @expose
     */
    this.attributeName_ = attributeName;

    /**
     * The minimum radius of the proportionality
     * @private
     * @type {number}
     * @api stable
     * @expose
     */
    this.minRadius_ = parseInt(minRadius) || 5;

    /**
     * The maximum radius of the proportionality
     * @private
     * @type {number}
     * @api stable
     * @expose
     */
    this.maxRadius_ = parseInt(maxRadius) || 15;

    /**
     * The style point define by user
     * @private
     * @type {M.Style}
     * @api stable
     * @expose
     */
    this.style_ = style;

    /**
     * the proportionality function
     * @private
     * @type {function}
     * @api stable
     * @expose
     */
    this.proportionalFunction_ = proportionalFunction || ((value, minValue, maxValue, minRadius, maxRadius) =>
      (((value - minValue) * (maxRadius - minRadius)) / (maxValue - minValue)) + minRadius);

    /**
     * @public
     * @type {Array<M.Feature>}
     * @api stable
     * @expose
     */
    this.layerFeatures_ = [];

    if (this.maxRadius_ < this.minRadius_) {
      this.minRadius_ = maxRadius;
      this.maxRadius_ = minRadius;
    }

    goog.base(this, options, {});
  });

  goog.inherits(M.style.Proportional, M.Style);

  /**
   * This function apply the style to specified layer
   * @function
   * @public
   * @param {M.Layer.Vector} layer - Layer where to apply choropleth style
   * @api stable
   */
  M.style.Proportional.prototype.apply = function(layer) {
    this.layer_ = layer;
    this.update_();
  };

  /**
   * This function returns the attribute name defined by user
   * @function
   * @public
   * @return {String} attribute name of Style
   * @api stable
   */
  M.style.Proportional.prototype.getAttributeName = function() {
    return this.attributeName_;
  };

  /**
   * This function set the attribute name defined by user
   * @function
   * @public
   * @param {String} attributeName - attribute name to set
   * @api stable
   */
  M.style.Proportional.prototype.setAttributeName = function(attributeName) {
    this.attributeName_ = attributeName;
    this.update_();
    return this;
  };

  /**
   * This function returns the style point defined by user
   * @function
   * @public
   * @return {M.style.Point} style point of each feature
   */
  M.style.Proportional.prototype.getStyle = function() {
    return this.style_;
  };

  /**
   * This function set the style point defined by user
   * @function
   * @public
   * @param {M.style.Point} style - style point to set
   * @api stable
   */
  M.style.Proportional.prototype.setStyle = function(style) {
    this.style_ = style;
    this.update_();
    return this;
  };

  /**
   * This function get the minimum radius of the style point
   * @function
   * @public
   * @return {number} minimum radius of style point
   * @api stable
   */
  M.style.Proportional.prototype.getMinRadius = function() {
    return this.minRadius_;
  };

  /**
   * This function set proportional function
   * @function
   * @public
   * @param {function} proportionalFunction - proportional function
   * @api stable
   */
  M.style.Proportional.prototype.setProportionalFunction = function(proportionalFunction) {
    this.proportionalFunction_ = proportionalFunction;
    this.update_();
  };

  /**
   * This function get proportional function
   * @function
   * @public
   * @return {number} minimum radius of style point
   * @api stable
   */
  M.style.Proportional.prototype.getProportionalFunction = function() {
    return this.proportionalFunction_;
  };

  /**
   * This function set the minimum radius of the style point
   * @function
   * @public
   * @param {number} minRadius - minimum radius of style point
   * @api stable
   */
  M.style.Proportional.prototype.setMinRadius = function(minRadius) {
    this.minRadius_ = parseInt(minRadius);
    if (minRadius >= this.maxRadius_) {
      // this.maxRadius_ = minRadius + 10;
      M.exception("No puede establecerse un radio mínimo mayor que el máximo.");
    }
    this.update_();
    return this;
  };

  /**
   * This function get the maximum radius of the style point
   * @function
   * @public
   * @return {number} maximum radius of style point
   * @api stable
   */
  M.style.Proportional.prototype.getMaxRadius = function() {
    return this.maxRadius_;
  };

  /**
   * This function set the maximum radius of the style point
   * @function
   * @public
   * @param {number} minRadius - maximum radius of style point
   * @api stable
   */
  M.style.Proportional.prototype.setMaxRadius = function(maxRadius) {
    this.maxRadius_ = parseInt(maxRadius);
    if (maxRadius <= this.minRadius_) {
      // this.minRadius_ = maxRadius - 10;
      M.exception("No puede establecerse un radio máximo menor que el mínimo.");
    }
    this.update_();
    return this;
  };

  /**
   * This function updates the canvas of style
   *
   * @function
   * @public
   * @api stable
   */
  M.style.Proportional.prototype.updateCanvas = function() {
    this.updateCanvasPromise_ = new Promise((success, fail) => {
      if (!M.utils.isNullOrEmpty(this.layer_)) {
        let style = !M.utils.isNullOrEmpty(this.style_) ? this.style_ : this.layer_.getStyle();

        if (style instanceof M.style.Simple) {
          let featureStyle = style.clone();
          if (!(featureStyle instanceof M.style.Point)) {
            featureStyle = new M.style.Point(featureStyle.options_);
          }
          let sizeAttribute = M.style.Proportional.getSizeAttribute_(featureStyle);

          let styleMax = featureStyle.clone();
          let styleMin = featureStyle.clone();
          let maxRadius = this.getMaxRadius();
          let minRadius = this.getMinRadius();
          styleMax.set(sizeAttribute, maxRadius);
          styleMin.set(sizeAttribute, minRadius);

          this.loadCanvasImage_(maxRadius, styleMax.toImage(), (canvasImageMax) => {
            this.loadCanvasImage_(minRadius, styleMin.toImage(), (canvasImageMin) => {
              this.drawGeometryToCanvas(canvasImageMax, canvasImageMin, success);
            });
          });
        }
        else if (!M.utils.isNullOrEmpty(style)) {
          this.canvas_ = style.canvas_;
          success();
        }
      }
    });
  };

  /**
   * TODO
   *
   * @function
   * @public
   * @param {CanvasRenderingContext2D} vectorContext - context of style canvas
   * @api stable
   */
  M.style.Proportional.prototype.loadCanvasImage_ = function(value, url, callbackFn) {
    let image = new Image();
    image.crossOrigin = 'Anonymous';
    image.onload = function() {
      callbackFn({
        'image': this,
        'value': value
      });
    };
    image.onerror = function() {
      callbackFn({
        'value': value
      });
    };
    image.src = url;
  };

  /**
   * TODO
   *
   * @function
   * @public
   * @param {CanvasRenderingContext2D} vectorContext - context of style canvas
   * @api stable
   */
  M.style.Proportional.prototype.drawGeometryToCanvas = function(canvasImageMax, canvasImageMin, callbackFn) {
    let maxImage = canvasImageMax['image'];
    let minImage = canvasImageMin['image'];

    this.canvas_.height = maxImage.height + 5 + minImage.height + 5;
    let vectorContext = this.canvas_.getContext('2d');
    vectorContext.textBaseline = "middle";

    // MAX VALUE

    let coordXText = 0;
    let coordYText = 0;
    if (!M.utils.isNullOrEmpty(maxImage)) {
      coordXText = maxImage.width + 5;
      coordYText = maxImage.height / 2;
      if (/^https?\:\/\//i.test(maxImage.src)) {
        this.canvas_.height = 80 + 40 + 10;
        vectorContext.fillText(`  max: ${this.maxValue_}`, 85, 40);
        vectorContext.drawImage(maxImage, 0, 0, 80, 80);
      }
      else {
        vectorContext.fillText(`  max: ${this.maxValue_}`, coordXText, coordYText);
        vectorContext.drawImage(maxImage, 0, 0);
      }
    }

    // MIN VALUE

    if (!M.utils.isNullOrEmpty(minImage)) {
      let coordinateX = 0;
      if (!M.utils.isNullOrEmpty(maxImage)) {
        coordinateX = (maxImage.width / 2) - (minImage.width / 2);
      }
      let coordinateY = maxImage.height + 5;
      coordYText = coordinateY + (minImage.height / 2);
      if (/^https?\:\/\//i.test(minImage.src)) {
        vectorContext.fillText(`  min: ${this.minValue_}`, 85, 105);
        vectorContext.drawImage(minImage, 20, 85, 40, 40);
      }
      else {
        vectorContext.fillText(`  min: ${this.minValue_}`, coordXText, coordYText);
        vectorContext.drawImage(minImage, coordinateX, coordinateY);
      }
    }
    callbackFn();
  };

  /**
   * This function updates the style
   * @function
   * @private
   * @api stable
   */
  M.style.Proportional.prototype.update_ = function() {
    if (!M.utils.isNullOrEmpty(this.layer_)) {
      let features = this.layer_.getFeatures();
      let [minRadius, maxRadius] = [this.minRadius_, this.maxRadius_];
      [this.minValue_, this.maxValue_] = M.style.Proportional.getMinMaxValues_(features, this.attributeName_);
      features.forEach(function(feature) {
        let style;
        if (!M.utils.isNullOrEmpty(this.style_)) {
          style = this.style_.clone();
        }
        else {
          let featureStyle = feature.getStyle();
          if (!M.utils.isNullOrEmpty(featureStyle)) {
            style = featureStyle.clone();
          }
          else {
            style = this.layer_.getStyle().clone();
          }
        }
        let featureStyle = style;
        if (!(featureStyle instanceof M.style.Point)) {
          featureStyle = new M.style.Point(featureStyle.options_);
        }
        style = this.calculateStyle_(feature, {
          minRadius: minRadius,
          maxRadius: maxRadius,
          minValue: this.minValue_,
          maxValue: this.maxValue_,
        }, featureStyle);
        feature.setStyle(style);
      }, this);
      this.updateCanvas();
    }
  };

  /**
   * This function gets the min value of feature's atributte.
   * @function
   * @private
   * @param {Array<M.Feature>} features - array of features
   * @param {String} attributeName - attributeName of style
   * @api stable
   */
  M.style.Proportional.getMinMaxValues_ = function(features, attributeName) {
    let [minValue, maxValue] = [undefined, undefined];
    let filteredFeatures = features.filter(feature =>
      ![NaN, undefined, null].includes(feature.getAttribute(attributeName))).map(f => parseInt(f.getAttribute(attributeName)));
    let index = 1;
    if (!M.utils.isNullOrEmpty(filteredFeatures)) {
      minValue = filteredFeatures[0];
      maxValue = filteredFeatures[0];
      while (index < filteredFeatures.length - 1) {
        let posteriorValue = filteredFeatures[index + 1];
        minValue = (minValue < posteriorValue) ? minValue : posteriorValue;
        maxValue = (maxValue < posteriorValue) ? posteriorValue : maxValue;
        index++;
      }
    }
    return [minValue, maxValue];
  };

  /**
   * This function returns the attribute of style point that controls the size
   * @function
   * @private
   * @return {string} the attribute that controls the size
   * @api stable
   */
  M.style.Proportional.getSizeAttribute_ = function(style) {
    let sizeAttribute = 'radius';
    if (!M.utils.isNullOrEmpty(style.get('icon'))) {
      if (!M.utils.isNullOrEmpty(style.get('icon.src'))) {
        sizeAttribute = 'icon.scale';
      }
      else {
        sizeAttribute = 'icon.radius';
      }
    }
    return sizeAttribute;
  };

  /**
   * This function returns the proportional style of feature
   * @function
   * @private
   * @param {M.Feature} feature
   * @param {object} options - minRadius, maxRadius, minValue, maxValue
   * @param {M.style.Point} style
   * @return {M.style.Simple} the proportional style of feature
   * @api stable
   */
  M.style.Proportional.prototype.calculateStyle_ = function(feature, options, style) {
    if (!M.utils.isNullOrEmpty(style)) {
      let [minRadius, maxRadius] = [options.minRadius, options.maxRadius];
      if (!M.utils.isNullOrEmpty(style.get('icon.src'))) {
        minRadius = options.minRadius / M.style.Proportional.SCALE_PROPORTION;
        maxRadius = options.maxRadius / M.style.Proportional.SCALE_PROPORTION;
      }
      let value = feature.getAttribute(this.attributeName_);
      if (value == null) {
        console.warn(`Warning: ${this.attributeName_} value is null or empty.`);
      }
      let radius = this.proportionalFunction_(value, options.minValue, options.maxValue,
        minRadius, maxRadius);
      let zindex = options.maxValue - parseFloat(feature.getAttribute(this.attributeName_));
      style.set(M.style.Proportional.getSizeAttribute_(style), radius);
      style.set('zindex', zindex);
    }
    return style;
  };

  /**
   * This constant defines the scale proportion for iconstyle in styleproportional.
   * @constant
   * @public
   * @api stable
   */
  M.style.Proportional.SCALE_PROPORTION = 20;
})();