Map/DisplayVariablesConcept.js

"use strict";

import i18next from "i18next";
/*global require*/

var defined = require("terriajs-cesium/Source/Core/defined").default;
var defaultValue = require("terriajs-cesium/Source/Core/defaultValue").default;
var knockout = require("terriajs-cesium/Source/ThirdParty/knockout").default;

var flattenNested = require("../Core/flattenNested");
var inherit = require("../Core/inherit");
var VariableConcept = require("./VariableConcept");

/**
 * Represents a concept which contains a list of variables which can be used to change the appearance of data.
 * A DisplayVariablesConcept contains an items array of VariableConcepts.
 *
 * @alias DisplayVariablesConcept
 * @constructor
 * @extends VariableConcept
 * @param {String} [name='Display Variable'] Display name of this concept.
 * @param {Boolean|Object} [options] Options; for backwards compatibility, if a boolean is passed, it is interpreted as options.allowMultiple. Also, VariableConcept options are passed through.
 * @param {Function} [getColorCallback] For backwards compatibility, a third argument is interpreted as options.getColorCallback. Use options.getColorCallback in preference.
 * @param {Boolean} [options.allowMultiple=false] Set to true if more than one checkbox can be selected at a time.
 * @param {Boolean} [options.requireSomeActive=false] Set to true if at least one checkbox must be selected at all times.
 * @param {Function} [options.getColorCallback] A function with no arguments that returns a color for the VariableConcept. If undefined, no color is set.
 * @param {Concept[]} [options.items] The array of concepts contained in this group. Each item's parent property is overridden with `this`.
 * @param {Boolean} [options.isOpen] Whether this concept item is currently open.
 * @param {Boolean} [options.isSelectable] Whether this node is selectable.
 * @param {String[]} [options.exclusiveChildIds] A list of child ids which cannot be selected at the same time as any other child. Default [].
 */
var DisplayVariablesConcept = function(name, options) {
  const that = this;
  name = defaultValue(
    name,
    i18next.t("map.displayVariablesConcept.defaultName")
  );

  options = defaultValue(options, defaultValue.EMPTY_OBJECT);
  VariableConcept.call(this, name, options);

  /**
   * Gets or sets a flag for whether more than one checkbox can be selected at a time.
   * Default false.
   * @type {Boolean}
   */
  this.allowMultiple = defaultValue(options.allowMultiple, false);

  /**
   * Gets or sets a flag for whether at least one checkbox must be selected at all times.
   * Default false.
   * @type {Boolean}
   */
  this.requireSomeActive = defaultValue(options.requireSomeActive, false);

  /**
   * Gets or sets a function with no arguments that returns a color for the VariableConcept. If undefined, no color is set (the default).
   * @type {Function}
   */
  this.getColorCallback = options.getColorCallback;

  /**
   * Gets or sets the array of concepts contained in this group.
   * If options.items is present, each item's parent property is overridden with `this`.
   * @type {Concept[]}
   */
  this.items = defaultValue(options.items, []);
  this.items.forEach(function(item) {
    item.parent = that;
  });

  /**
   * Gets or sets a value indicating whether this concept item is currently open.  When an
   * item is open, its child items (if any) are visible. Default true.
   * @type {Boolean}
   */
  this.isOpen = defaultValue(options.isOpen, true);

  /**
   * Gets or sets a flag indicating whether this node is selectable.
   * @type {Boolean}
   */
  this.isSelectable = defaultValue(options.isSelectable, false);

  /**
   * Gets or sets a list of child ids which cannot be selected at the same time as any other child. Defaults to [].
   * Only relevant with allowMultiple = true.
   * Eg. Suppose the child concepts have ids "10" for 10 year olds, etc, plus "ALL" for all ages,
   * "U21" and "21PLUS" for under and over 21 year olds.
   * Then by specifying ["ALL", "U21", "21PLUS"], when the user selects one of these values, any other values will be unselected.
   * And when the user selects any other value (eg. "10"), if any of these values were selected, they will be unselected.
   * @type {String[]}
   */
  this.exclusiveChildIds = defaultValue(options.exclusiveChildIds, []);

  /**
   * Gets or sets a function which is called whenever a child item is successfully toggled.
   * Its sole argument is the toggled child item.
   * @type {Function}
   */
  this.toggleActiveCallback = undefined;

  knockout.track(this, [
    "items",
    "isOpen",
    "allowMultiple",
    "requireSomeActive"
  ]);

  /**
   * Gets an array of currently active/selected items.
   * @return {VariableConcept[]} Array of active/selected items.
   */
  knockout.defineProperty(this, "activeItems", {
    get: function() {
      return this.items.filter(function(item) {
        return item.isActive;
      });
    }
  });
};

/**
 * Function that is called when a variable is selected or unselected.
 * @callback DisplayVariablesConcept~setSelected
 * @param {Integer} index Index of the selected variable in the list of variables.
 * @param {String} varName The name of the selected variable.
 * @param {Boolean} makeActive Whether to make this variable active or not.
 */

inherit(VariableConcept, DisplayVariablesConcept);

/**
 * Gets a flat array of all of this concept's descendants, including itself, which meet a given condition.
 * @param {Function} condition A function of a concept, which returns a boolean.
 * @return {Concept[]}
 */
DisplayVariablesConcept.prototype.getNodes = function(condition) {
  return flattenNested(getNestedNodes(this, condition));
};

// Returns a nested array containing only the leaf concepts, at their actual depths in the tree of nodes.
function getNestedNodes(concept, condition) {
  if (condition(concept)) {
    return concept;
  }
  if (!concept.items) {
    return [];
  }
  return concept.items.map(child => getNestedNodes(child, condition));
}

/**
 * If appropriate, based on this.allowMultiple, this.requireSomeActive and this.exclusiveChildIds,
 * sets the {@link VariableConcept#isActive} property on all children, so that _only_ the provided concept is activated.
 * This is called by the VariableConcept's toggleActive function.
 * If not appropriate, simply toggles the concept without changing any others.
 * (For backwards compatibility, VariableConcept does not call this function if there's no possibility it's needed.)
 * @param {VariableConcept} concept The concept to make active/inactive.
 */
DisplayVariablesConcept.prototype.toggleActiveItem = function(concept) {
  var isActive = concept.isActive;
  // If we requireSomeActive, and we would wind up with no active items, do nothing.
  if (
    isActive &&
    this.requireSomeActive &&
    this.items.every(child =>
      child === concept ? child.isActive : !child.isActive
    )
  ) {
    return;
  }
  // If we are activating an item, but only allow one selection, we need to deselect the others.
  if (
    (!isActive && !this.allowMultiple) ||
    shouldClearActives(this, concept.id)
  ) {
    for (var i = 0; i < this.items.length; i++) {
      var target = this.items[i] === concept;
      if (this.items[i].isActive !== target) {
        this.items[i].isActive = target;
      }
    }
  } else {
    // It was not appropriate to clear all other active concepts, so just toggle this one.
    concept.isActive = !concept.isActive;
  }
  // Finally, if a callback is defined, call it.
  // Eg. This is used by charts to check if other charts with a different x column are currently displayed, and deactivate them.
  if (
    defined(
      this.toggleActiveCallback &&
        typeof this.toggleActiveCallback === "function"
    )
  ) {
    this.toggleActiveCallback(concept);
  }
};

function shouldClearActives(parent, selectedId) {
  // When we turn on an exclusive child, return true.
  if (parent.exclusiveChildIds.indexOf(selectedId) >= 0) {
    return true;
  }
  // Or, if any exclusive children are active, return true.
  var activeChildIds = parent.activeItems.map(function(child) {
    return child.id;
  });
  var activeExclusiveIds = activeChildIds.filter(function(activeId) {
    return parent.exclusiveChildIds.indexOf(activeId) >= 0;
  });
  return activeExclusiveIds.length > 0;
}

module.exports = DisplayVariablesConcept;