src_gui_stage.js

import {Point, Point3D} from '../math/point';
import {LayerGroup} from './layerGroup';

/**
 * Window/level binder.
 */
export class WindowLevelBinder {
  getEventType = function () {
    return 'wlchange';
  };
  getCallback = function (layerGroup) {
    return function (event) {
      const viewLayers = layerGroup.getViewLayersByDataIndex(event.dataid);
      if (viewLayers.length !== 0) {
        const vc = viewLayers[0].getViewController();
        vc.setWindowLevel(event.value[0], event.value[1]);
      }
    };
  };
}

/**
 * Position binder.
 */
export class PositionBinder {
  getEventType = function () {
    return 'positionchange';
  };
  getCallback = function (layerGroup) {
    return function (event) {
      const pointValues = event.value[1];
      const vc = layerGroup.getActiveViewLayer().getViewController();
      // handle different number of dimensions
      const currentPos = vc.getCurrentPosition();
      const currentDims = currentPos.length();
      const inputDims = pointValues.length;
      if (inputDims !== currentDims) {
        if (inputDims === currentDims - 1) {
          // add missing dim, for ex: input 3D -> current 4D
          pointValues.push(currentPos.get(currentDims - 1));
        } else if (inputDims === currentDims + 1) {
          // remove extra dim, for ex: input 4D -> current 3D
          pointValues.pop();
        }
      }
      vc.setCurrentPosition(new Point(pointValues));
    };
  };
}

/**
 * Zoom binder.
 */
export class ZoomBinder {
  getEventType = function () {
    return 'zoomchange';
  };
  getCallback = function (layerGroup) {
    return function (event) {
      const scale = {
        x: event.value[0],
        y: event.value[1],
        z: event.value[2]
      };
      let center;
      if (event.value.length === 6) {
        center = new Point3D(
          event.value[3],
          event.value[4],
          event.value[5]
        );
      }
      layerGroup.setScale(scale, center);
      layerGroup.draw();
    };
  };
}

/**
 * Offset binder.
 */
export class OffsetBinder {
  getEventType = function () {
    return 'offsetchange';
  };
  getCallback = function (layerGroup) {
    return function (event) {
      layerGroup.setOffset({
        x: event.value[0],
        y: event.value[1],
        z: event.value[2]
      });
      layerGroup.draw();
    };
  };
}

/**
 * Opacity binder. Only propagates to view layers of the same data.
 */
export class OpacityBinder {
  getEventType = function () {
    return 'opacitychange';
  };
  getCallback = function (layerGroup) {
    return function (event) {
      // exit if no data index
      if (typeof event.dataid === 'undefined') {
        return;
      }
      // propagate to first view layer
      const viewLayers = layerGroup.getViewLayersByDataIndex(event.dataid);
      if (viewLayers.length !== 0) {
        viewLayers[0].setOpacity(event.value);
        viewLayers[0].draw();
      }
    };
  };
}

/**
 * List of binders.
 */
export const binderList = {
  WindowLevelBinder,
  PositionBinder,
  ZoomBinder,
  OffsetBinder,
  OpacityBinder
};

/**
 * Stage: controls a list of layer groups and their
 * synchronisation.
 */
export class Stage {

  // associated layer groups
  #layerGroups = [];
  // active layer group index
  #activeLayerGroupIndex = null;

  // layer group binders
  #binders = [];
  // binder callbacks
  #callbackStore = null;

  /**
   * Get the layer group at the given index.
   *
   * @param {number} index The index.
   * @returns {LayerGroup} The layer group.
   */
  getLayerGroup(index) {
    return this.#layerGroups[index];
  }

  /**
   * Get the number of layer groups that form the stage.
   *
   * @returns {number} The number of layer groups.
   */
  getNumberOfLayerGroups() {
    return this.#layerGroups.length;
  }

  /**
   * Get the active layer group.
   *
   * @returns {LayerGroup} The layer group.
   */
  getActiveLayerGroup() {
    return this.getLayerGroup(this.#activeLayerGroupIndex);
  }

  /**
   * Get the view layers associated to a data index.
   *
   * @param {number} index The data index.
   * @returns {Array} The layers.
   */
  getViewLayersByDataIndex(index) {
    let res = [];
    for (let i = 0; i < this.#layerGroups.length; ++i) {
      res = res.concat(this.#layerGroups[i].getViewLayersByDataIndex(index));
    }
    return res;
  }

  /**
   * Get the draw layers associated to a data index.
   *
   * @param {number} index The data index.
   * @returns {Array} The layers.
   */
  getDrawLayersByDataIndex(index) {
    let res = [];
    for (let i = 0; i < this.#layerGroups.length; ++i) {
      res = res.concat(this.#layerGroups[i].getDrawLayersByDataIndex(index));
    }
    return res;
  }

  /**
   * Add a layer group to the list.
   *
   * @param {object} htmlElement The HTML element of the layer group.
   * @returns {LayerGroup} The newly created layer group.
   */
  addLayerGroup(htmlElement) {
    this.#activeLayerGroupIndex = this.#layerGroups.length;
    const layerGroup = new LayerGroup(htmlElement);
    // add to storage
    const isBound = this.#callbackStore && this.#callbackStore.length !== 0;
    if (isBound) {
      this.unbindLayerGroups();
    }
    this.#layerGroups.push(layerGroup);
    if (isBound) {
      this.bindLayerGroups();
    }
    // return created group
    return layerGroup;
  }

  /**
   * Get a layer group from an HTML element id.
   *
   * @param {string} id The element id to find.
   * @returns {LayerGroup} The layer group.
   */
  getLayerGroupByDivId(id) {
    return this.#layerGroups.find(function (item) {
      return item.getDivId() === id;
    });
  }

  /**
   * Set the layer groups binders.
   *
   * @param {Array} list The list of binder objects.
   */
  setBinders(list) {
    if (typeof list === 'undefined' || list === null) {
      throw new Error('Cannot set null or undefined binders');
    }
    if (this.#binders.length !== 0) {
      this.unbindLayerGroups();
    }
    this.#binders = list.slice();
    this.bindLayerGroups();
  }

  /**
   * Empty the layer group list.
   */
  empty() {
    this.unbindLayerGroups();
    for (let i = 0; i < this.#layerGroups.length; ++i) {
      this.#layerGroups[i].empty();
    }
    this.#layerGroups = [];
    this.#activeLayerGroupIndex = null;
  }

  /**
   * Reset the stage: calls reset on all layer groups.
   */
  reset() {
    for (let i = 0; i < this.#layerGroups.length; ++i) {
      this.#layerGroups[i].reset();
    }
  }

  /**
   * Draw the stage: calls draw on all layer groups.
   */
  draw() {
    for (let i = 0; i < this.#layerGroups.length; ++i) {
      this.#layerGroups[i].draw();
    }
  }

  /**
   * Synchronise the fit scale of the group layers.
   */
  syncLayerGroupScale() {
    let minScale;
    const hasScale = [];
    for (let i = 0; i < this.#layerGroups.length; ++i) {
      const scale = this.#layerGroups[i].calculateFitScale();
      if (typeof scale !== 'undefined') {
        hasScale.push(i);
        if (typeof minScale === 'undefined' || scale < minScale) {
          minScale = scale;
        }
      }
    }
    // exit if no scale
    if (typeof minScale === 'undefined') {
      return;
    }
    // apply min scale to layers
    for (let j = 0; j < this.#layerGroups.length; ++j) {
      if (hasScale.includes(j)) {
        this.#layerGroups[j].setFitScale(minScale);
      }
    }
  }

  /**
   * Bind the layer groups of the stage.
   */
  bindLayerGroups() {
    if (this.#layerGroups.length === 0 ||
      this.#layerGroups.length === 1 ||
      this.#binders.length === 0) {
      return;
    }
    // create callback store
    this.#callbackStore = new Array(this.#layerGroups.length);
    // add listeners
    for (let i = 0; i < this.#layerGroups.length; ++i) {
      for (let j = 0; j < this.#binders.length; ++j) {
        this.#addEventListeners(i, this.#binders[j]);
      }
    }
  }

  /**
   * Unbind the layer groups of the stage.
   */
  unbindLayerGroups() {
    if (this.#layerGroups.length === 0 ||
      this.#layerGroups.length === 1 ||
      this.#binders.length === 0 ||
      !this.#callbackStore) {
      return;
    }
    // remove listeners
    for (let i = 0; i < this.#layerGroups.length; ++i) {
      for (let j = 0; j < this.#binders.length; ++j) {
        this.#removeEventListeners(i, this.#binders[j]);
      }
    }
    // clear callback store
    this.#callbackStore = null;
  }

  /**
   * Get the binder callback function for a given layer group index.
   * The function is created if not yet stored.
   *
   * @param {object} binder The layer binder.
   * @param {number} index The index of the associated layer group.
   * @returns {Function} The binder function.
   */
  #getBinderCallback(binder, index) {
    if (typeof this.#callbackStore[index] === 'undefined') {
      this.#callbackStore[index] = [];
    }
    const store = this.#callbackStore[index];
    let binderObj = store.find(function (elem) {
      return elem.binder === binder;
    });
    if (typeof binderObj === 'undefined') {
      // create new callback object
      binderObj = {
        binder: binder,
        callback: (event) => {
          // stop listeners
          this.#removeEventListeners(index, binder);
          // apply binder
          binder.getCallback(this.#layerGroups[index])(event);
          // re-start listeners
          this.#addEventListeners(index, binder);
        }
      };
      this.#callbackStore[index].push(binderObj);
    }
    return binderObj.callback;
  }

  /**
   * Add event listeners for a given layer group index and binder.
   *
   * @param {number} index The index of the associated layer group.
   * @param {object} binder The layer binder.
   */
  #addEventListeners(index, binder) {
    for (let i = 0; i < this.#layerGroups.length; ++i) {
      if (i !== index) {
        this.#layerGroups[index].addEventListener(
          binder.getEventType(),
          this.#getBinderCallback(binder, i)
        );
      }
    }
  }

  /**
   * Remove event listeners for a given layer group index and binder.
   *
   * @param {number} index The index of the associated layer group.
   * @param {object} binder The layer binder.
   */
  #removeEventListeners(index, binder) {
    for (let i = 0; i < this.#layerGroups.length; ++i) {
      if (i !== index) {
        this.#layerGroups[index].removeEventListener(
          binder.getEventType(),
          this.#getBinderCallback(binder, i)
        );
      }
    }
  }

} // class Stage