matrix/models/connectiondataview.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 { typecheckInstance } from '../../utils/typecheck.js';
import {
  initSubscriptions,
  addSubscription,
  unsubscribeSubscriptions,
  combineSubscriptions,
} from '../../utils/subscriptions.js';
import {
  initSubscribers,
  addSubscriber,
  removeSubscriber,
  callSubscribers,
} from '../../utils/subscribers.js';
import { VirtualTreeDataView } from './virtualtreedataview.js';
import { Events } from '../../events.js';
import { callContinuationIf } from './helpers.js';
import { GroupData } from './group.js';

// last argument will be a callback
function subscribeMany(apis, cb) {
  const values = new Array(apis.length);
  const has_value = apis.map(() => false);
  let has_all_values = false;
  let subscriptions = initSubscriptions();

  apis
    .map((api, i) => {
      return api((value) => {
        values[i] = value;
        if (!has_all_values) {
          has_value[i] = true;
          has_all_values = !has_value.includes(false);
          if (!has_all_values) return;
        }

        cb(...values);
      });
    })
    .forEach((sub) => {
      subscriptions = addSubscription(subscriptions, sub);
    });

  return () => {
    subscriptions = unsubscribeSubscriptions(subscriptions);
  };
}

export function resizeArrayMod(array, length, offset, create, remove) {
  if (length === array.length) return;

  const tmp = array.slice();

  array.length = length;

  // copy existing entries from their previous position
  for (let i = 0; i < Math.min(tmp.length, length); i++) {
    const index = offset + i;

    array[index % length] = tmp[index % tmp.length];
  }

  if (tmp.length < length) {
    for (let i = tmp.length; i < length; i++) {
      const index = i + offset;

      array[index % length] = create ? create(index) : void 0;
    }
  } else if (remove) {
    for (let i = length; i < tmp.length; i++) {
      const index = i + offset;

      remove(tmp[index % tmp.length]);
    }
  }
}

function subtractMod(a, b, n) {
  let result = (a - b) % n;

  if (result < 0) result += n;

  return result;
}

/**
 * A dataview for connections.
 */
export class ConnectionDataView extends Events {
  _addSubscription(sub) {
    this.subscriptions = addSubscription(this.subscriptions, sub);
  }

  _notifyPair(row_element, column_element, connection) {
    const rows = this.rows;
    const columns = this.columns;
    const matrix = this.matrix;
    const subscribers = this.subscribers;

    const startIndex1 = this.startIndex1;
    const startIndex2 = this.startIndex2;

    const n = rows.indexOf(row_element);

    if (n === -1) return;

    const m = columns.indexOf(column_element);

    if (m === -1) return;

    matrix[n][m] = connection;

    let index1 = startIndex1 + subtractMod(n, startIndex1, rows.length);
    let index2 = startIndex2 + subtractMod(m, startIndex2, columns.length);

    callSubscribers(
      this.subscribers,
      index1,
      index2,
      connection,
      row_element,
      column_element
    );
  }

  _lowRegisterConnection(row_element, column_element, connection) {
    let connections_for = this.connections.get(row_element);

    if (!connections_for) {
      this.connections.set(row_element, (connections_for = new Map()));
    }

    if (
      row_element instanceof GroupData ||
      column_element instanceof GroupData
    ) {
      let set = connections_for.get(column_element);

      if (!set) {
        connections_for.set(column_element, (set = new Set()));
      }

      set.add(connection);

      this._notifyPair(row_element, column_element, set);
    } else {
      connections_for.set(column_element, connection);

      this._notifyPair(row_element, column_element, connection);
    }
  }

  _lowUnregisterConnection(row_element, column_element, connection) {
    let connections_for = this.connections.get(row_element);

    if (connections_for === void 0) return;

    if (
      row_element instanceof GroupData ||
      column_element instanceof GroupData
    ) {
      let set = connections_for.get(column_element);

      if (set === void 0) return;

      set.delete(connection);

      if (set.size === 0) {
        connections_for.delete(column_element);
        set = void 0;
      }

      this._notifyPair(row_element, column_element, set);
    } else {
      connections_for.delete(column_element);
      this._notifyPair(row_element, column_element, void 0);
    }

    if (connections_for.size === 0) {
      this.connections.delete(row_element);
    }
  }

  _forOrderedElements(a, b, cb) {
    const elements1 = this.elements1;
    const elements2 = this.elements2;

    if (elements1.has(a) && elements2.has(b)) {
      cb(a, b);
    }
    if (elements2.has(a) && elements1.has(b)) {
      cb(b, a);
    }
  }

  _registerConnection(connection) {
    this._forOrderedElements(
      connection.from,
      connection.to,
      (row_element, column_element) => {
        const elements1 = this.elements1;
        const elements2 = this.elements2;

        this._lowRegisterConnection(row_element, column_element, connection);

        for (
          let group = row_element.parent;
          elements1.has(group);
          group = group.parent
        ) {
          this._lowRegisterConnection(group, column_element, connection);
        }

        for (
          let group = column_element.parent;
          elements2.has(group);
          group = group.parent
        ) {
          this._lowRegisterConnection(row_element, group, connection);
        }
      }
    );
  }

  _unregisterConnection(connection) {
    this._forOrderedElements(
      connection.from,
      connection.to,
      (row_element, column_element) => {
        const elements1 = this.elements1;
        const elements2 = this.elements2;

        this._lowUnregisterConnection(row_element, column_element, connection);

        for (
          let group = row_element.parent;
          elements1.has(group);
          group = group.parent
        ) {
          this._lowUnregisterConnection(group, column_element, connection);
        }

        for (
          let group = column_element.parent;
          elements2.has(group);
          group = group.parent
        ) {
          this._lowUnregisterConnection(row_element, group, connection);
        }
      }
    );
  }

  _registerConnectionFor(node) {
    this.virtualtreeview1.matrix
      .getConnectionsOf(node)
      .forEach((connection) => {
        this._registerConnection(connection);
      });
  }

  _unregisterConnectionFor(node) {
    this.virtualtreeview1.matrix
      .getConnectionsOf(node)
      .forEach((connection) => {
        this._unregisterConnection(connection);
      });
  }

  _subscribeAllElements(virtualtreeview, dst, other) {
    const filter = virtualtreeview.filterFunction;
    const subscribe = (group) => {
      return group.forEachAsync((node) => {
        // jshint -W123
        return callContinuationIf(node, filter, (node) => {
          // jshint +W123
          dst.add(node);

          if (node instanceof GroupData) {
            let subscriptions = subscribe(node);

            return addSubscription(subscriptions, () => dst.delete(node));
          } else {
            this._registerConnectionFor(node);

            return () => {
              this._unregisterConnectionFor(node);
              dst.delete(node);
            };
          }
        });
      });
    };

    return subscribe(virtualtreeview.group);
  }

  get startIndex1() {
    return this.virtualtreeview1.startIndex;
  }

  get startIndex2() {
    return this.virtualtreeview2.startIndex;
  }

  get amount1() {
    return this.virtualtreeview1.amount;
  }

  get amount2() {
    return this.virtualtreeview2.amount;
  }

  constructor(virtualtreeview1, virtualtreeview2) {
    typecheckInstance(virtualtreeview1, VirtualTreeDataView);
    typecheckInstance(virtualtreeview2, VirtualTreeDataView);

    if (virtualtreeview1.matrix !== virtualtreeview2.matrix)
      throw new Error(
        'Can only work with ListDataView instance of the same matrix.'
      );

    super();
    this.virtualtreeview1 = virtualtreeview1;
    this.virtualtreeview2 = virtualtreeview2;

    this.elements1 = new Set();
    this.elements2 = new Set();
    // map of all connections. also contains the list of
    // connections in each group as a set
    this.connections = new Map();
    this.subscriptions = initSubscriptions();

    // subscribers to subscribeElements
    this.subscribers = initSubscribers();

    // matrix of currently visible connections
    this.matrix = [];

    // elements, e.g. ports or groups in the rows
    this.rows = [];

    // elements, e.g. ports or groups in the columns
    this.columns = [];

    // maintain the matrix
    this._addSubscription(
      this.subscribeAmount((rows, columns) => {
        resizeArrayMod(this.rows, rows, this.startIndex1);

        resizeArrayMod(this.columns, columns, this.startIndex2);

        resizeArrayMod(
          this.matrix,
          rows,
          this.startIndex1,
          () => new Array(columns)
        );

        this.matrix.forEach((row) => {
          resizeArrayMod(row, columns, this.startIndex2);
        });
      })
    );

    this._addSubscription(
      virtualtreeview1.subscribeElements((index, row_element) => {
        const rows = this.rows;
        const columns = this.columns;
        const matrix = this.matrix;
        const subscribers = this.subscribers;

        const startIndex2 = this.startIndex2;
        const i = index % rows.length;

        rows[i] = row_element;

        for (let n = 0; n < columns.length; n++) {
          const j = (startIndex2 + n) % columns.length;
          const column_element = columns[j];

          let connection = this.getConnectionFor(row_element, column_element);

          // actually update and tell our subscribers
          matrix[i][j] = connection;

          callSubscribers(
            subscribers,
            index,
            startIndex2 + n,
            connection,
            row_element,
            column_element
          );
        }
      })
    );

    this._addSubscription(
      virtualtreeview2.subscribeElements((index, column_element) => {
        const rows = this.rows;
        const columns = this.columns;
        const matrix = this.matrix;
        const subscribers = this.subscribers;

        const startIndex1 = this.startIndex1;
        const j = index % columns.length;

        columns[j] = column_element;

        for (let n = 0; n < rows.length; n++) {
          const i = (startIndex1 + n) % rows.length;
          const row_element = rows[i];

          let connection = this.getConnectionFor(row_element, column_element);

          // actually update and tell our subscribers
          matrix[i][j] = connection;

          callSubscribers(
            subscribers,
            startIndex1 + n,
            index,
            connection,
            row_element,
            column_element
          );
        }
      })
    );

    this._addSubscription(
      this._subscribeAllElements(
        virtualtreeview1,
        this.elements1,
        this.elements2
      )
    );
    this._addSubscription(
      this._subscribeAllElements(
        virtualtreeview2,
        this.elements2,
        this.elements1
      )
    );

    // collect all connections
    {
      const matrix = this.virtualtreeview1.matrix;

      matrix.forEachConnection((connection) => {
        this._registerConnection(connection);
      });

      this._addSubscription(
        matrix.subscribe('connectionAdded', (connection) => {
          this._registerConnection(connection);
        })
      );

      this._addSubscription(
        matrix.subscribe('connectionRemoved', (connection) => {
          this._unregisterConnection(connection);
        })
      );
    }

    virtualtreeview1.subscribeScrollView((offset) => {
      this.emit('scrollView', offset, 0);
    });

    virtualtreeview2.subscribeScrollView((offset) => {
      this.emit('scrollView', 0, offset);
    });
  }

  getConnectionFor(row_element, column_element) {
    if (!row_element || !column_element) return void 0;

    const connections_for = this.connections.get(row_element);

    if (!connections_for) return void 0;

    return connections_for.get(column_element);
  }

  subscribeSize(cb) {
    const virtualtreeview1 = this.virtualtreeview1;
    const virtualtreeview2 = this.virtualtreeview2;

    return subscribeMany(
      [
        virtualtreeview1.subscribeSize.bind(virtualtreeview1),
        virtualtreeview2.subscribeSize.bind(virtualtreeview2),
      ],
      cb
    );
  }

  subscribeAmount(cb) {
    const virtualtreeview1 = this.virtualtreeview1;
    const virtualtreeview2 = this.virtualtreeview2;

    return subscribeMany(
      [
        virtualtreeview1.subscribeAmount.bind(virtualtreeview1),
        virtualtreeview2.subscribeAmount.bind(virtualtreeview2),
      ],
      cb
    );
  }

  subscribeElements(cb) {
    this.subscribers = addSubscriber(this.subscribers, cb);

    const matrix = this.matrix;
    const rows = this.rows;
    const columns = this.columns;
    const subscribers = this.subscribers;

    const startIndex1 = this.startIndex1;
    const startIndex2 = this.startIndex2;

    for (let n = 0; n < rows.length; n++) {
      const i = (startIndex1 + n) % rows.length;
      const row_element = rows[i];

      for (let m = 0; m < columns.length; m++) {
        const j = (startIndex2 + m) % columns.length;
        const column_element = columns[j];

        callSubscribers(
          cb,
          startIndex1 + n,
          startIndex2 + m,
          matrix[i][j],
          row_element,
          column_element
        );
      }
    }

    return () => {
      this.subscribers = removeSubscriber(this.subscribers, cb);
    };
  }

  subscribeScrollView(cb) {
    return this.subscribe('scrollView', cb);
  }

  destroy() {
    super.destroy();
    this.subscriptions = unsubscribeSubscriptions(this.subscriptions);
  }

  _observeConnectionsFor(element, list1, list2, callback) {
    const matrix = list1.matrix;

    if (!list1.includes(element)) throw new Error('Could not find child.');

    const result = new Set();

    matrix.getConnectionsOf(element).forEach((connection) => {
      const other = connection.to === element ? connection.from : connection.to;

      if (!list2.includes(other)) return;
      result.add(connection);
    });

    callSubscribers(callback, result);

    return combineSubscriptions(
      matrix.subscribe('connectionRemoved', (connection) => {
        if (connection.to !== element && connection.from !== element) return;

        const other =
          connection.to === element ? connection.from : connection.to;
        if (!list2.includes(other)) return;

        result.delete(connection);
        callSubscribers(callback, result);
      }),
      matrix.subscribe('connectionAdded', (connection) => {
        if (connection.to !== element && connection.from !== element) return;

        const other =
          connection.to === element ? connection.from : connection.to;
        if (!list2.includes(other)) return;

        result.add(connection);
        callSubscribers(callback, result);
      })
    );
  }

  observeConnectionsForRow(row_element, callback) {
    return this._observeConnectionsFor(
      row_element,
      this.listview1,
      this.listview2,
      callback
    );
  }

  observeConnectionsForColumn(column_element, callback) {
    return this._observeConnectionsFor(
      column_element,
      this.listview2,
      this.listview1,
      callback
    );
  }
}