matrix/models/matrix.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 { Datum } from './datum.js';
import { PortData } from './port.js';
import { GroupData } from './group.js';
import { VirtualTreeDataView } from './virtualtreedataview.js';
import { TreeNodeData } from './treenode.js';
import { ConnectionData } from './connection.js';

/**
 * Represents a matrix, which consists of a tree of groups and port objects;
 * and a set of connections between ports.
 */
export class MatrixData extends Datum {
  /**
   */
  constructor() {
    super();
    this.root = this.createGroup();
    this.nodes = new Map();
    this.last_id = 0;

    // Map<PortData, Map<PortData, ConnectionData>>
    this.connections = new Map();
  }

  registerNode(node) {
    if (!(node instanceof TreeNodeData))
      throw new TypeError('Expected TreeDataNode');

    const id = node.id;

    const nodes = this.nodes;

    if (nodes.has(id)) throw new Error('Node with same id already defined.');

    nodes.set(id, node);
  }

  unregisterNode(node) {
    if (!(node instanceof TreeNodeData))
      throw new TypeError('Expected TreeDataNode');

    const id = node.id;
    const nodes = this.nodes;

    if (nodes.get(id) !== node) throw new Error('Removing unknown node.');

    this.getConnectionsOf(node).forEach((connection) => {
      this.deleteConnection(connection);
    });

    nodes.delete(id);
  }

  /**
   * Creates a port object for this matrix.
   *
   * @param {Object} port - The data for the port object.
   * @param {Function} [PortClass=PortData] - The port data class to use.
   */
  createPort(port, PortClass) {
    if (!(port instanceof PortData)) {
      if (!PortClass) PortClass = PortData;
      if (!port) port = {};
      if (!port.id) {
        port.id = this.last_id++;
      }
      port = new PortClass(this, port);
    }

    return port;
  }

  /**
   * Creates a group object for this matrix.
   *
   * @param {Object} groupd - The data for the group object.
   * @param {Function} [GroupClass=GroupData] - The group data class to use.
   */
  createGroup(group, GroupClass) {
    if (!(group instanceof GroupData)) {
      if (!GroupClass) GroupClass = GroupData;
      if (!group) group = {};
      if (!group.id) {
        group.id = this.last_id++;
      }
      group = new GroupClass(this, group);
      if (!(group instanceof GroupData))
        throw new TypeError('GroupClass must extend GroupData.');
    }

    return group;
  }

  /**
   * Add a group to this matrix. Will be added at the top level of the tree.
   */
  addGroup(group) {
    return this.root.addGroup(group);
  }

  /**
   * Remove a group from the top level of the tree.
   */
  deleteGroup(group) {
    return this.root.deleteGroup(group);
  }

  /**
   * Find a group object for the given id.
   *
   * @param {any} id.
   */
  getGroupById(id) {
    const group = this.nodes.get(id);

    if (group && group instanceof GroupData) return group;
  }

  // APIs for managing ports

  /**
   * Add a port to this matrix. Will be added at the top level of the tree.
   */
  addPort(port) {
    return this.root.addPort(port);
  }

  /**
   * Remove a port from the top level of the tree.
   */
  deletePort(port) {
    this.root.deletePort(port);
  }

  /**
   * Find a port object for the given id.
   *
   * @param {any} id.
   */
  getPortById(id) {
    const port = this.nodes.get(id);

    if (port && port instanceof PortData) return port;
  }

  _lowRegisterConnection(from, to, connection) {
    let connections_from = this.connections.get(from);

    if (!connections_from) {
      connections_from = new Map();
      this.connections.set(from, connections_from);
    }

    if (connections_from.has(to)) {
      throw new Error('Connection already exists.');
    }

    let connections_from_to = connections_from.get(to);

    connections_from.set(to, connection);
  }

  _lowUnregisterConnection(from, to, connection) {
    let connections_from = this.connections.get(from);

    if (connections_from) {
      const con = connections_from.get(to);

      if (con === connection) {
        connections_from.delete(to);

        if (!connections_from.size) {
          this.connections.delete(from);
        }

        return;
      }
    }

    throw new Error('Could not find connection.');
  }

  _registerConnection(connection) {
    this._lowRegisterConnection(connection.from, connection.to, connection);

    if (connection.to !== connection.from)
      this._lowRegisterConnection(connection.to, connection.from, connection);
    this.emit('connectionAdded', connection);
  }

  _unregisterConnection(connection) {
    this._lowUnregisterConnection(connection.from, connection.to, connection);
    if (connection.to !== connection.from)
      this._lowUnregisterConnection(connection.to, connection.from, connection);
    this.emit('connectionRemoved', connection);
  }

  /**
   * Create a connection object.
   */
  createConnection(connection) {
    if (!(connection instanceof ConnectionData)) {
      connection = new ConnectionData(this, connection);
    }

    return connection;
  }

  /**
   * Add a connection object.
   */
  addConnection(connection) {
    connection = this.createConnection(connection);

    this._registerConnection(connection);

    return connection;
  }

  /**
   * Delete a connection object.
   */
  deleteConnection(connection) {
    this._unregisterConnection(connection);
  }

  /**
   * Connect two ports.
   *
   * @param {PortData} from
   * @param {PortData} to
   */
  connect(from, to) {
    return this.addConnection({
      from: from,
      to: to,
    });
  }

  /**
   * Return all connections of the given node.
   */
  getConnectionsOf(node) {
    const connections_map = this.connections.get(node);

    return connections_map ? Array.from(connections_map.values()) : [];
  }

  /**
   * Return all connections from this node.
   */
  getConnectionsFrom(node) {
    return this.getConnectionsOf(node).filter(
      (connection) => connection.from === node
    );
  }

  /**
   * Return all connections to this node.
   */
  getConnectionsTo(node) {
    return this.getConnectionsOf(node).filter(
      (connection) => connection.to === node
    );
  }

  /**
   * Return the connection of the two given ports, if any exists.
   */
  getConnection(a, b) {
    const connections_map = this.connections.get(a);

    if (!connections_map) return;

    return connections_map.get(b);
  }

  /**
   * Iterate all connections.
   */
  forEachConnection(cb) {
    this.connections.forEach((map, port) => {
      map.forEach((connection) => {
        if (connection.from === port) cb(connection);
      });
    });
  }

  // other public apis

  createVirtualTreeDataView(amount, filterFunction, sortFunction) {
    return new VirtualTreeDataView(
      this.root,
      amount,
      filterFunction,
      sortFunction
    );
  }
}