Models/FunctionParameter.js

"use strict";

/*global require*/
var arraysAreEqual = require("../Core/arraysAreEqual");
var defaultValue = require("terriajs-cesium/Source/Core/defaultValue").default;
var defined = require("terriajs-cesium/Source/Core/defined").default;

var DeveloperError = require("terriajs-cesium/Source/Core/DeveloperError")
  .default;
var knockout = require("terriajs-cesium/Source/ThirdParty/knockout").default;
var runLater = require("../Core/runLater");
var serializeToJson = require("../Core/serializeToJson");
var updateFromJson = require("../Core/updateFromJson");
var TerriaError = require("../Core/TerriaError");
var i18next = require("i18next").default;

/**
 * A parameter to a {@link CatalogFunction}.
 *
 * @alias FunctionParameter
 * @constructor
 * @abstract
 *
 * @param {Object} [options] Object with the following properties:
 * @param {Terria} options.terria The Terria instance.
 * @param {CatalogFunction} options.catalogFunction The function that this is a parameter to.
 * @param {String} options.id The unique ID of this parameter.
 * @param {String} [options.name] The name of this parameter.  If not specified, the ID is used as the name.
 * @param {String} [options.description] The description of the parameter.
 * @param {Boolean} [options.isRequired] True if this parameter is required, false if it is optional.
 * @param {Object} [options.value] The initial value of the parameter.
 */
var FunctionParameter = function(options) {
  if (!defined(options) || !defined(options.terria)) {
    throw new DeveloperError("options.terria is required.");
  }

  if (!defined(options.catalogFunction)) {
    throw new DeveloperError("options.catalogFunction is required.");
  }

  if (!defined(options.id)) {
    throw new DeveloperError("options.id is required.");
  }

  this._terria = options.terria;
  this._catalogFunction = options.catalogFunction;
  this._id = options.id;

  this._loadingPromise = undefined;
  this._lastLoadInfluencingValues = undefined;

  /**
   * Gets or sets a value indicating whether the parameter is currently loading.  This property
   * is observable.
   * @type {Boolean}
   */
  this.isLoading = false;

  /**
   * Gets or sets the name of the parameter.
   * @type {String}
   */
  this.name = defaultValue(options.name, options.id);

  /**
   * Gets or sets the description of the parameter.
   * @type {String}
   */
  this.description = options.description;

  /**
   * Gets or sets a value indicating whether this parameter is required.
   * @type {Boolean}
   * @default false
   */
  this.isRequired = defaultValue(options.isRequired, false);

  /**
   * A converter that can be used to convert this parameter for use with a {@link CatalogFunction}.
   * The actual type and content of this property is defined by the catalog function.
   * @type {Any}
   */
  this.converter = undefined;

  /**
   * Gets or sets the formatter (from the set defined by {@link FunctionParameter#availableFormatters})
   * to use to format this parameter to pass to the {@link CatalogFunction}.
   * @type {String}
   */
  this.formatter = "default";

  /**
   * Gets the default value for this parameter, or undefined if there is no default value.
   */
  this._defaultValue = options.defaultValue;

  /**
   * Gets or sets the current value of this parameter.
   */
  this.value = options.value;

  knockout.track(this, ["_value", "formatter", "_defaultValue"]);

  /**
   * Gets or sets the current value of this parameter.
   * @member {*} value
   * @memberof FunctionParameter.prototype
   */
  knockout.defineProperty(this, "value", {
    get: FunctionParameter.defaultValueGetter,
    set: FunctionParameter.defaultValueSetter
  });

  /**
   * Gets the default value for this parameter, or undefined if there is no default value.
   * @member {*} defaultValue
   * @memberof FunctionParameter.prototype
   */
  knockout.defineProperty(this, "defaultValue", {
    get: function() {
      return this._defaultValue;
    },
    set: function(value) {
      this._defaultValue = value;
    }
  });
};

FunctionParameter.defaultValueGetter = function() {
  if (!defined(this._value)) {
    return this.defaultValue;
  }
  return this._value;
};

FunctionParameter.defaultValueSetter = function(value) {
  this._value = value;
};

Object.defineProperties(FunctionParameter.prototype, {
  /**
   * Gets the type of this parameter.
   * @memberof FunctionParameter.prototype
   * @type {String}
   */
  type: {
    get: function() {
      throw new DeveloperError(
        "FunctionParameter.type must be overridden in derived classes."
      );
    }
  },

  /**
   * Gets the Terria instance associated with this parameter.
   * @memberof FunctionParameter.prototype
   * @type {Terria}
   */
  terria: {
    get: function() {
      return this._terria;
    }
  },

  /**
   * Gets the function to which this is a parameter.
   * @memberof FunctionParameter.prototype
   * @type {CatalogFunction}
   */
  catalogFunction: {
    get: function() {
      return this._catalogFunction;
    }
  },

  /**
   * Gets the ID of the parameter.
   * @memberof FunctionParameter.prototype
   * @type {String}
   */
  id: {
    get: function() {
      return this._id;
    }
  },

  /**
   * Gets the formatters that are available to format the parameter's value.
   * @memberof FunctionParameter.prototype
   */
  availableFormatters: {
    get: function() {
      if (this.constructor && this.constructor.AvailableFormatters) {
        return this.constructor.AvailableFormatters;
      } else {
        return {
          default: this.formatValueAsString.bind(this)
        };
      }
    }
  }
});

FunctionParameter.prototype.load = function() {
  if (!defined(this._load)) {
    // No loading required.
    return undefined;
  }

  if (defined(this._loadingPromise)) {
    // Load already in progress.
    return this._loadingPromise;
  }

  var loadInfluencingValues = [];
  if (defined(this._getValuesThatInfluenceLoad)) {
    loadInfluencingValues = this._getValuesThatInfluenceLoad();
  }

  if (arraysAreEqual(loadInfluencingValues, this._lastLoadInfluencingValues)) {
    // Already loaded, and nothing has changed to force a re-load.
    return undefined;
  }

  this.isLoading = true;

  var that = this;
  this._loadingPromise = runLater(function() {
    that._lastLoadInfluencingValues = [];
    if (defined(that._getValuesThatInfluenceLoad)) {
      that._lastLoadInfluencingValues = that._getValuesThatInfluenceLoad();
    }

    return that._load();
  })
    .then(function() {
      that._loadingPromise = undefined;
      that.isLoading = false;
    })
    .otherwise(function(e) {
      that._lastLoadInfluencingValues = undefined;
      that._loadingPromise = undefined;
      that.isLoading = false;
      throw e;
    });

  return this._loadingPromise;
};

/**
 * Represents value as string.
 * @param {Object} [value] Value to format as string. If not specified, {@link FunctionParameter#value} is used.
 * @return {String} String representation of the value.
 */
FunctionParameter.prototype.formatValueAsString = function(value) {
  value = defaultValue(value, this._value);
  return defined(value) ? value.toString() : "-";
};

/**
 * Formats this value to pass to a service. The format is controlled by the
 * {@link FunctionParameter#formatter} property.
 *
 * @param {Object} [value] Value to format as string. If not specified, {@link FunctionParameter#value} is used.
 * @return {Any} The formatted value.
 */
FunctionParameter.prototype.formatForService = function(value) {
  const formatter = this.availableFormatters[this.formatter];
  if (!formatter) {
    throw new TerriaError({
      sender: this,
      title: i18next.t("models.function.unknownFormaterTitle"),
      message:
        i18next.t("models.function.unknownFormaterMessage") +
        "   * " +
        Object.keys(this.availableFormatters).join("\n   * ")
    });
  }

  value = defaultValue(value, this._value);
  return formatter(value);
};

/**
 * Updates the function parameter from a JSON object-literal description of it.
 *
 * @param {Object} json The JSON description.  The JSON should be in the form of an object literal, not a string.
 * @returns {Promise} A promise that resolves when the update is complete.
 */
FunctionParameter.prototype.updateFromJson = function(json) {
  return updateFromJson(this, json);
};

/**
 * Serializes the data item to JSON.
 *
 * @return {Object} The serialized JSON object-literal.
 */
FunctionParameter.prototype.serializeToJson = function() {
  var result = serializeToJson(this);
  result.type = this.type;
  return result;
};

module.exports = FunctionParameter;