matrix/models/group.js

/*
 * This file is part of AUX.
 *
 * AUX is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public
 * License as published by the Free Software Foundation; either
 * version 3 of the License, or (at your option) any later version.
 *
 * AUX is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * General Public License for more details.
 *
 * You should have received a copy of the GNU General
 * Public License along with this program; if not, write to the
 * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
 * Boston, MA  02110-1301  USA
 */

/**
 * @module matrix
 */

import { TreeNodeData } from './treenode.js';
import { PortData } from './port.js';
import {
  initSubscriptions,
  addSubscription,
  unsubscribeSubscriptions,
} from '../../utils/subscriptions.js';

function onChildTreechanged() {
  this.parent.emit('treeChanged');
}

/**
 * The data model of a group.
 */
export class GroupData extends TreeNodeData {
  /**
   * This property is true.
   */
  get isGroup() {
    return true;
  }

  constructor(matrix, o) {
    super(matrix, o);
    this.children = new Set();
  }

  /**
   * Add a child to this group.
   *
   * @emits childAdded
   * @emits treeChanged
   */
  addChild(child) {
    if (!(child instanceof TreeNodeData))
      throw new TypeError('Expected TreeDataNode');

    if (this.isChildOf(child))
      throw new Error('Creating graphs is not allowed.');

    this.matrix.registerNode(child);

    const children = this.children;

    child.setParent(this);
    children.add(child);

    this.emit('childAdded', child);
    this.emit('treeChanged', this);

    child.on('treeChanged', onChildTreechanged);
  }

  /**
   * Removed a child node.
   *
   * @emits childRemoved
   * @emits treeChanged
   */
  deleteChild(child) {
    if (!(child instanceof TreeNodeData))
      throw new TypeError('Expected TreeDataNode');

    const children = this.children;

    if (!children.has(child)) throw new Error('Unknown child.');

    this.matrix.unregisterNode(child);

    child.setParent(null);
    children.delete(child);

    child.off('treeChanged', onChildTreechanged);

    this.emit('childRemoved', child);
    this.emit('treeChanged', this);
  }

  /**
   * Adds a port to this group.
   */
  addPort(port) {
    if (!(port instanceof PortData)) port = this.matrix.createPort(port);

    this.addChild(port);
    return port;
  }

  /**
   * Removes a port from this group.
   */
  deletePort(port) {
    this.deleteChild(port);
  }

  /**
   * Adds a group to this group.
   */
  addGroup(group) {
    if (!(group instanceof GroupData)) group = this.matrix.createGroup(group);

    this.addChild(group);
    return group;
  }

  /**
   * Removes a group from this group.
   */
  deleteGroup(group) {
    this.deleteChild(group);
  }

  /**
   * Iterates all children of this group.
   *
   * @param {Function} cb - The callback function to invoke with each child
   *  node.
   */
  forEach(cb) {
    this.children.forEach(cb);
  }

  /**
   * Iterates all children of this group and all their children recursively.
   * Will descend into group nodes if the callback returns either undefined or
   * a trueish value.
   *
   * @param cb {Function} - A callback function to be called for each node in
   *  this subtree. The arguments are the node and the path description. The
   *  path description is an Array of objects with 3 properties `parent`,
   *  `index` and `length`. Parent is the parent node, `index` the index of
   *  the node in the list of children and `length` is the number of siblings.
   * @param sorter {Function} - An optional sort function. Will be used as an
   *  argument to Array.prototype.sort for each list of child nodes.
   */
  forEachNode(cb, sorter, path) {
    const children = Array.from(this.children);

    if (sorter) {
      children.sort(sorter);
    }

    if (!Array.isArray(path)) {
      path = [];
    }

    for (let i = 0; i < children.length; i++) {
      const child = children[i];
      const current_path = path.concat([
        {
          parent: this,
          index: i,
          length: children.length,
        },
      ]);

      const retval = cb(child, current_path);

      if (retval !== void 0 && !retval) continue;

      if (child instanceof GroupData) {
        child.forEachNode(cb, sorter, current_path);
      }
    }
  }

  /**
   * Call a function for each child node asynchronously. Will subscribe to
   * new children being added and call the callback. The callback function
   * may return a subscription which is removed once the corresponding child
   * node is removed from this group.
   *
   * @returns {Function} - A subscription. Call this function to unsubscribe.
   */
  forEachAsync(callback) {
    let subs = initSubscriptions();
    const child_subscriptions = new Map();

    this.children.forEach((node) => {
      child_subscriptions.set(node, callback(node) || null);
    });

    subs = addSubscription(
      subs,
      this.subscribe('childAdded', (child) => {
        child_subscriptions.set(child, callback(child) || null);
      })
    );

    subs = addSubscription(
      subs,
      this.subscribe('childRemoved', (child) => {
        unsubscribeSubscriptions(child_subscriptions.get(child));
        child_subscriptions.delete(child);
      })
    );

    return () => {
      subs = unsubscribeSubscriptions(subs);
      child_subscriptions.forEach((sub) => unsubscribeSubscriptions(sub));
      child_subscriptions.clear();
    };
  }
}