predefined/layouts.js

'use strict';

const { constants } = require('../shared.js');

/**
 * Factories for predefined layout handlers.
 *
 * @namespace layouts
 * @memberof module:predefined
 */

/**
 * Places users around a table, with the given amount of overlap. The first user
 * will be the "table", and their position when they join is stamped as the
 * outline of the table. The next four users are positioned, facing inwards,
 * around the four sides of the table.
 *
 * @memberof module:predefined.layouts
 *
 * @param {number} overlap
 */
class TableLayout {
  TABLE = 0;
  BOTTOM = 1;
  LEFT = 2;
  TOP = 3;
  RIGHT = 4;

  constructor(overlap) {
    if (overlap == undefined) {
      // or if overlap is null, since using == instead of ===
      throw new Error('Overlap must be defined for TableLayout.');
    }
    this.overlap = overlap;
    this.table = null;
    this.bottomLeft = null;
    this.bottomRight = null;
    this.topLeft = null;
    this.topRight = null;
  }

  layoutTable(view, device) {
    this.table = view;
    this.bottomLeft = view.bottomLeft;
    this.bottomRight = view.bottomRight;
    this.topLeft = view.topLeft;
    this.topRight = view.topRight;
  }

  layoutBottom(view, device) {
    view.moveTo(this.bottomLeft.x, this.bottomLeft.y - this.overlap);
    device.moveTo(this.bottomLeft.x, this.bottomLeft.y - this.overlap);
  }

  layoutLeft(view, device) {
    view.moveTo(this.topLeft.x + this.overlap, this.topLeft.y);
    view.rotateBy(constants.ROTATE_90);
    device.moveTo(this.topLeft.x + this.overlap, this.topLeft.y);
    device.rotateBy(constants.ROTATE_90);
  }

  layoutTop(view, device) {
    view.moveTo(this.topRight.x, this.topRight.y + this.overlap);
    view.rotateBy(constants.ROTATE_180);
    device.moveTo(this.topRight.x, this.topRight.y + this.overlap);
    device.rotateBy(constants.ROTATE_180);
  }

  layoutRight(view, device) {
    view.moveTo(this.bottomRight.x - this.overlap, this.bottomRight.y);
    view.rotateBy(-constants.ROTATE_90);
    device.moveTo(this.bottomRight.x - this.overlap, this.bottomRight.y);
    device.rotateBy(-constants.ROTATE_90);
  }

  /**
   * Apply the layout to a newly connected view and its device.
   *
   * @param {module:server.ServerView} view
   * @param {module:server.Device} device
   */
  layout(view, device) {
    const index = view.index > 0 ? (view.index % 4) + 1 : 0;
    console.log('INDEX', index);
    if (index == this.TABLE) {
      console.log('TABLE', view.index);
      this.layoutTable(view, device);
      return;
    }
    if (this.table == null) {
      // console.log('TABLE NOT SET', view.index);
      setTimeout(this.layout.bind(this, view, device), 0);
      return;
    }
    switch (index) {
      case this.BOTTOM:
        console.log('BOTTOM', view.index);
        this.layoutBottom(view, device);
        break;
      case this.LEFT:
        console.log('LEFT', view.index);
        this.layoutLeft(view, device);
        break;
      case this.TOP:
        console.log('TOP', view.index);
        this.layoutTop(view, device);
        break;
      case this.RIGHT:
        console.log('RIGHT', view.index);
        this.layoutRight(view, device);
        break;
    }
  }
}

/**
 * Places users in a line, with the given amount of overlap. Best used with
 * either server-side gestures or when users are unable to manipulate their
 * views.
 * - Valid for use with server-side gestures.
 *
 * @memberof module:predefined.layouts
 *
 * @param {number} overlap
 */
class LineLayout {
  constructor(overlap) {
    if (overlap == undefined) {
      // or if overlap is null, since using == instead of ===
      throw new Error('Overlap must be defined for LineLayout.');
    }
    this.overlap = overlap;
    this.views = [];
    this.deviceRights = [];
  }

  /**
   * Apply the layout to a newly connected view and its device.
   *
   * @param {module:server.ServerView} view
   * @param {module:server.Device} device
   */
  layout(view, device) {
    if (this.views.length > 0) {
      // Challenge with the layout is to position the new view correctly in the
      // workspace, but also mirror that position correctly in the device space,
      // so that the relative position of the views matches the relative
      // position of the devices.
      const prev = this.views[this.views.length - 1];
      const change = prev.transformPointChange(this.overlap, 0);
      const anchor = prev.topRight.minus(change);
      view.moveTo(anchor.x, anchor.y);
      view.rotation = prev.rotation;

      const side = this.deviceRights[this.deviceRights.length - 1] - this.overlap;
      device.moveTo(side, 0);
      this.deviceRights.push(side + device.width);
      this.views.push(view);
    } else {
      this.deviceRights[0] = device.width;
      this.views[0] = view;
    }
    view.on('disconnected', () => {
      const index = this.views.indexOf(view);
      this.views.splice(index, 1);
      this.deviceRights.splice(index, 1);
    });
  }
}

/**
 * @private
 * @deprecated
 * @param {number} overlap
 * @returns {TableLayout}
 * @memberof module:predefined.layouts
 */
function table(overlap) {
  console.warn('WARNING: `table(overlap)` is deprecated, use `new TableLayout(overlap)` instead.');
  return new TableLayout(overlap);
}

/**
 * @private
 * @deprecated
 * @param {number} overlap
 * @returns {LineLayout}
 * @memberof module:predefined.layouts
 */
function line(overlap) {
  console.warn('WARNING: `line(overlap)` is deprecated, use `new LineLayout(overlap)` instead.');
  return new LineLayout(overlap);
}

module.exports = {
  LineLayout,
  TableLayout,
  line,
  table,
};