Source: CGArray.js

//////////////////////////////////////////////////////////////////////////////
// CGArray
//////////////////////////////////////////////////////////////////////////////

/**
 * CGArray is essentially an array for holding CGV Objects. Any method
 * that works on an Array will also work on a CGArray.
 *
 * If a single array is provided it will be converted to an CGArray.
 * If mulitple elements are provided, they will be added to the new CGArray.
 *
 * ### Examples
 * ```js
 * const a1 = new CGArray(1, 2, 3);
 * => CGArray [1,2,3]
 *
 * const a2 = new CGArray([1,2,3])
 * => CGArray [1,2,3]
 * ```
 */
class CGArray extends Array {

  constructor(...items) {
    let elements = items;
    if ( (items.length === 1) && (Array.isArray(items[0])) ) {
      elements = items[0];
    }
    if (elements.length === 1) {
      super();
      this.push(elements[0]);
    } else if (elements.length > 20000) {
      // Note: 50,000 was too large, so we're trying 40,000
      // Note: 40,000 was too large (on Chrome), so now we're trying 30,000 - 2022-02-22
      // Note: 30,000 was too large (on Chrome), so now we're trying 20,000 - 2022-05-03
      super();
      for (let i = 0, len = elements.length; i < len; i++) {
        this.push(elements[i]);
      }
    } else {
      super(...elements);
    }
  }

  /**
 * Return the string 'CGArray'
 * @return {String}
 */
  toString() {
    return 'CGArray';
  }

  /**
 * Returns true if the CGArray is not empty.
 * @return {Boolean}
 */
  present() {
    return this.length > 0;
  }

  /**
 * Return true if the CGArray is empty.
 * @return {Boolean}
 */
  empty() {
    return this.length === 0;
  }

  /**
 * Returns new CGArray with element removed
 * @return {CGArray}
 */
  remove(element) {
    return this.filter( i => i !== element );
  }

  /**
   * Return the first element of the CGArray or "undefined" if the array is empty
   * @return {Element|or|undefined}
   */
  get first() {
    return this[0];
  }

  /**
   * Return the last element of the CGArray or "undefined" if the array is empty
   * @return {Element|or|undefined}
   */
  get last() {
    return this[this.length - 1];
  }

  // FIXME: return an CGArray with a single element of 0 when it should be empty
  // FIXME: Using Polyfill for now
  // filter(selector) {
    // return new CGV.CGArray(Array.prototype.filter(selector));
  // }

  filter(func, thisArg) {
    if ( ! ((typeof func === 'Function' || typeof func === 'function') && this) )
      throw new TypeError();

    let len = this.length >>> 0,
        res = new Array(len), // preallocate array
        t = this, c = 0, i = -1;
    if (thisArg === undefined){
      while (++i !== len){
        // checks to see if the key was set
        if (i in this){
          if (func(t[i], i, t)){
            res[c++] = t[i];
          }
        }
      }
    } else {
      while (++i !== len){
        // checks to see if the key was set
        if (i in this){
          if (func.call(thisArg, t[i], i, t)){
            res[c++] = t[i];
          }
        }
      }
    }

    res.length = c; // shrink down array to proper size
    return new CGArray(res);
  }

  /**
   * @private
   */
  // FIXME: return an CGArray with a single element of 0 when it should be empty
  // FIXME: Using Polyfill for now
  // https://github.com/jonathantneal/array-flat-polyfill/blob/master/src/flat.js
  flat() {
    var self = this;
    var depth = isNaN(arguments[0]) ? 1 : Number(arguments[0]);
    return depth ? Array.prototype.reduce.call(this, function (acc, cur) {
      if (Array.isArray(cur)) {
        acc.push.apply(acc, self.flat.call(cur, depth - 1));
      } else {
        acc.push(cur);
      }

      return acc;
    }, []) : Array.prototype.slice.call(this);
  }

  /**
   * @private
   */
  map(...rest) {
    return (this.length === 0) ? this : super.map(...rest);
  }

  /**
   * Move the an item from oldIndex to newIndex.
   * @param {Number} oldIndex - index of element to move
   * @param {Number} newIndex - move element to this index
   */
  move(oldIndex, newIndex) {
    if (newIndex >= this.length) {
      let k = newIndex - this.length;
      while ((k--) + 1) {
        this.push(undefined);
      }
    }
    this.splice(newIndex, 0, this.splice(oldIndex, 1)[0]);
    return this;
  }

  /**
   * Retrieve subset of CGArray or an individual element from CGArray depending on term provided.
   * To find elements by cgvID use [Viewer.objects](Viewer.html#objects) instead.
   * Term      | Returns
   * ----------|----------------
   * undefined | Full CGArray
   * Ingeter   | The element at the index (base-1)
   * String    | First element with an 'name' property same as string or undefined
   * Array     | CGArray with elements with matching 'name' property
   *
   * @param {Integer|String|Array} term - The values returned depend on the term (see above table).
   * @return {CGArray|or|Element}
   */
  get(term) {
    if (term === undefined) {
      return this;
    } else if (Number.isInteger(term)) {
      return this[term - 1];
    } else if (typeof term === 'string') {
      return this.filter( element => element.name && element.name.toLowerCase() === term.toLowerCase() )[0];
    } else if (Array.isArray(term)) {
      return this.filter( element => term.some( name => element.name === name ) );
    } else {
      return new CGArray();
    }
  }

  /**
   * Return new CGArray with no duplicated values.
   * @return {CGArray}
   */
  unique() {
  // return new CGArray(this.filter( onlyUnique ));
    return CGArray.from(new Set(this));
  }

  /**
   * Change one or more properties of each element of the CGArray.
   * ```javascript
   * my_cgarray.attr(property, value)
   * my_cgarray.attr( {property1: value1, property2: value2} )
   * ```
   *
   * @param {Property|Value} attributes A property name and the new value.
   * @param {Object}     attributes An object properties and their new values.
   * @return {CGArray}
   */
  attr(attributes) {
    if ( (arguments.length === 1) && (typeof attributes === 'object') ) {
      const keys = Object.keys(attributes);
      const keyLen = keys.length;
      for (let i = 0, len = this.length; i < len; i++) {
        for (let j = 0; j < keyLen; j++) {
          this[i][keys[j]] = attributes[keys[j]];
        }
      }
    } else if (arguments.length === 2) {
      for (let i = 0, len = this.length; i < len; i++) {
        this[i][arguments[0]] = arguments[1];
      }
    } else if (attributes !== undefined) {
      throw new Error('attr(): must be 2 arguments or a single object');
    }
    return this;
  }

  /**
  * iterates through each element of the cgarray and run the callback.
  * In the callback _this_ will refer to the element.
  * ```javascript
  * .each(function(index, element))
  * ```
  *
  * Note: This is slower then a _forEach_ or a _for loop_ directly on the set.
  * @param {Function} callback Callback run on each element of CGArray.
  *   The callback will be called with 2 parameters: the index of the element
  *   and the element itself.
  * @return {CGArray}
  */
  // NOTE: it may feel better if this was (item, index) not (index, item)
  each(callback) {
    for (let i = 0, len = this.length; i < len; i++) {
      callback.call(this[i], i, this[i]);
    }
    return this;
  }

  /**
   * Return the CGArray as an Array
   * @return {Array}
   * @private
   */
  asArray() {
    return Array.from(this);
  }

  /**
   * Returns the object incased as a CGArray. If it's already a CGArray, it is returned untouched.
   * Helpfull to handle method parameters that can submit a single object or a CGArray of objects.
   * @param {Object} object
   *
   * @return {CGArray}
   */
  static arrayerize (object) {
    return (object.toString() === 'CGArray') ? object : new CGArray(object);
  }

  /** @ignore */

  /**
 * Sorts the CGArray by the provided property name.
 * @param {String} property Property to order each element set by [default: 'center']
 * @param {Boolean} descending Order in descending order (default: false)
 * @return {CGArray}
 */
  // orderBy(property, descending) {
  //   // Sort by function call
  //   if (this.length > 0) {
  //
  //     if (typeof this[0][property] === 'function'){
  //       this.sort(function(a,b) {
  //         if (a[property]() > b[property]()) {
  //           return 1;
  //         } else if (a[property]() < b[property]()) {
  //           return -1;
  //         } else {
  //           return 0;
  //         }
  //       })
  //     } else {
  //     // Sort by property
  //       this.sort(function(a,b) {
  //         if (a[property] > b[property]) {
  //           return 1;
  //         } else if (a[property] < b[property]) {
  //           return -1;
  //         } else {
  //           return 0;
  //         }
  //       })
  //     }
  //   }
  //   if (descending) this.reverse();
  //   return this;
  // }


}

export default CGArray;