src_gui_drawLayer.js

// namespaces
var dwv = dwv || {};
/** @namespace */
dwv.gui = dwv.gui || {};

/**
 * The Konva namespace.
 *
 * @external Konva
 * @see https://konvajs.org/
 */
var Konva = Konva || {};

/**
 * Draw layer.
 *
 * @param {HTMLElement} containerDiv The layer div, its id will be used
 *   as this layer id.
 * @class
 */
dwv.gui.DrawLayer = function (containerDiv) {

  // specific css class name
  containerDiv.className += ' drawLayer';

  // closure to self
  var self = this;

  // konva stage
  var konvaStage = null;

  /**
   * The layer base size as {x,y}.
   *
   * @private
   * @type {object}
   */
  var baseSize;

  /**
   * The layer base spacing as {x,y}.
   *
   * @private
   * @type {object}
   */
  var baseSpacing;

  /**
   * The layer fit scale.
   *
   * @private
   * @type {object}
   */
  var fitScale = {x: 1, y: 1};

  /**
   * The base layer offset.
   *
   * @private
   * @type {object}
   */
  var baseOffset = {x: 0, y: 0};

  /**
   * The view offset.
   *
   * @private
   * @type {object}
   */
  var viewOffset = {x: 0, y: 0};

  /**
   * The zoom offset.
   *
   * @private
   * @type {object}
   */
  var zoomOffset = {x: 0, y: 0};

  /**
   * The flip offset.
   *
   * @private
   * @type {object}
   */
  var flipOffset = {x: 0, y: 0};

  /**
   * The draw controller.
   *
   * @private
   * @type {object}
   */
  var drawController = null;

  /**
   * The plane helper.
   *
   * @private
   * @type {object}
   */
  var planeHelper;

  /**
   * The associated data index.
   *
   * @private
   * @type {number}
   */
  var dataIndex = null;

  /**
   * Get the associated data index.
   *
   * @returns {number} The index.
   */
  this.getDataIndex = function () {
    return dataIndex;
  };

  /**
   * Listener handler.
   *
   * @type {object}
   * @private
   */
  var listenerHandler = new dwv.utils.ListenerHandler();

  /**
   * Get the Konva stage.
   *
   * @returns {object} The stage.
   */
  this.getKonvaStage = function () {
    return konvaStage;
  };

  /**
   * Get the Konva layer.
   *
   * @returns {object} The layer.
   */
  this.getKonvaLayer = function () {
    // there should only be one layer
    return konvaStage.getLayers()[0];
  };

  /**
   * Get the draw controller.
   *
   * @returns {object} The controller.
   */
  this.getDrawController = function () {
    return drawController;
  };

  /**
   * Set the plane helper.
   *
   * @param {object} helper The helper.
   */
  this.setPlaneHelper = function (helper) {
    planeHelper = helper;
  };

  // common layer methods [start] ---------------

  /**
   * Get the id of the layer.
   *
   * @returns {string} The string id.
   */
  this.getId = function () {
    return containerDiv.id;
  };

  /**
   * Get the layer base size (without scale).
   *
   * @returns {object} The size as {x,y}.
   */
  this.getBaseSize = function () {
    return baseSize;
  };

  /**
   * Get the layer opacity.
   *
   * @returns {number} The opacity ([0:1] range).
   */
  this.getOpacity = function () {
    return konvaStage.opacity();
  };

  /**
   * Set the layer opacity.
   *
   * @param {number} alpha The opacity ([0:1] range).
   */
  this.setOpacity = function (alpha) {
    konvaStage.opacity(Math.min(Math.max(alpha, 0), 1));
  };

  /**
   * Add a flip offset along the layer X axis.
   */
  this.addFlipOffsetX = function () {
    // flip scale is handled by layer group
    // flip offset
    var scale = konvaStage.scale();
    var size = konvaStage.size();
    flipOffset.x += size.width / scale.x;
    // apply
    var offset = konvaStage.offset();
    offset.x += flipOffset.x;
    konvaStage.offset(offset);
  };

  /**
   * Add a flip offset along the layer Y axis.
   */
  this.addFlipOffsetY = function () {
    // flip scale is handled by layer group
    // flip offset
    var scale = konvaStage.scale();
    var size = konvaStage.size();
    flipOffset.y += size.height / scale.y;
    // apply
    var offset = konvaStage.offset();
    offset.y += flipOffset.y;
    konvaStage.offset(offset);
  };

  /**
   * Set the layer scale.
   *
   * @param {object} newScale The scale as {x,y}.
   * @param {dwv.math.Point3D} center The scale center.
   */
  this.setScale = function (newScale, center) {
    var orientedNewScale = planeHelper.getTargetOrientedPositiveXYZ(newScale);
    var finalNewScale = {
      x: fitScale.x * orientedNewScale.x,
      y: fitScale.y * orientedNewScale.y
    };

    var offset = konvaStage.offset();

    if (Math.abs(newScale.x) === 1 &&
      Math.abs(newScale.y) === 1 &&
      Math.abs(newScale.z) === 1) {
      // reset zoom offset for scale=1
      var resetOffset = {
        x: offset.x - zoomOffset.x,
        y: offset.y - zoomOffset.y
      };
      // store new offset
      zoomOffset = {x: 0, y: 0};
      konvaStage.offset(resetOffset);
    } else {
      if (typeof center !== 'undefined') {
        var worldCenter = planeHelper.getPlaneOffsetFromOffset3D({
          x: center.getX(),
          y: center.getY(),
          z: center.getZ()
        });
        // center was obtained with viewLayer.displayToMainPlanePos
        // compensated for baseOffset
        // TODO: justify...
        worldCenter = {
          x: worldCenter.x + baseOffset.x,
          y: worldCenter.y + baseOffset.y
        };

        var newOffset = dwv.gui.getScaledOffset(
          offset, konvaStage.scale(), finalNewScale, worldCenter);

        var newZoomOffset = {
          x: zoomOffset.x + newOffset.x - offset.x,
          y: zoomOffset.y + newOffset.y - offset.y
        };
        // store new offset
        zoomOffset = newZoomOffset;
        konvaStage.offset(newOffset);
      }
    }

    konvaStage.scale(finalNewScale);
    // update labels
    updateLabelScale(finalNewScale);
  };

  /**
   * Set the layer offset.
   *
   * @param {object} newOffset The offset as {x,y}.
   */
  this.setOffset = function (newOffset) {
    var planeNewOffset = planeHelper.getPlaneOffsetFromOffset3D(newOffset);
    konvaStage.offset({
      x: planeNewOffset.x +
        viewOffset.x + baseOffset.x + zoomOffset.x + flipOffset.x,
      y: planeNewOffset.y +
        viewOffset.y + baseOffset.y + zoomOffset.y + flipOffset.y
    });
  };

  /**
   * Set the base layer offset. Updates the layer offset.
   *
   * @param {dwv.math.Vector3D} scrollOffset The scroll offset vector.
   * @param {dwv.math.Vector3D} planeOffset The plane offset vector.
   * @returns {boolean} True if the offset was updated.
   */
  this.setBaseOffset = function (scrollOffset, planeOffset) {
    var scrollIndex = planeHelper.getNativeScrollIndex();
    var newOffset = planeHelper.getPlaneOffsetFromOffset3D({
      x: scrollIndex === 0 ? scrollOffset.getX() : planeOffset.getX(),
      y: scrollIndex === 1 ? scrollOffset.getY() : planeOffset.getY(),
      z: scrollIndex === 2 ? scrollOffset.getZ() : planeOffset.getZ(),
    });
    var needsUpdate = baseOffset.x !== newOffset.x ||
      baseOffset.y !== newOffset.y;
    // reset offset if needed
    if (needsUpdate) {
      var offset = konvaStage.offset();
      konvaStage.offset({
        x: offset.x - baseOffset.x + newOffset.x,
        y: offset.y - baseOffset.y + newOffset.y
      });
      baseOffset = newOffset;
    }
    return needsUpdate;
  };

  /**
   * Display the layer.
   *
   * @param {boolean} flag Whether to display the layer or not.
   */
  this.display = function (flag) {
    containerDiv.style.display = flag ? '' : 'none';
  };

  /**
   * Check if the layer is visible.
   *
   * @returns {boolean} True if the layer is visible.
   */
  this.isVisible = function () {
    return containerDiv.style.display === '';
  };

  /**
   * Draw the content (imageData) of the layer.
   * The imageData variable needs to be set
   */
  this.draw = function () {
    konvaStage.draw();
  };

  /**
   * Initialise the layer: set the canvas and context
   *
   * @param {object} size The image size as {x,y}.
   * @param {object} spacing The image spacing as {x,y}.
   * @param {number} index The associated data index.
   */
  this.initialise = function (size, spacing, index) {
    // set locals
    baseSize = size;
    baseSpacing = spacing;
    dataIndex = index;

    // create stage
    konvaStage = new Konva.Stage({
      container: containerDiv,
      width: baseSize.x,
      height: baseSize.y,
      listening: false
    });
    // reset style
    // (avoids a not needed vertical scrollbar)
    konvaStage.getContent().setAttribute('style', '');

    // create layer
    var konvaLayer = new Konva.Layer({
      listening: false,
      visible: true
    });
    konvaStage.add(konvaLayer);

    // create draw controller
    drawController = new dwv.ctrl.DrawController(konvaLayer);
  };

  /**
   * Fit the layer to its parent container.
   *
   * @param {number} fitScale1D The 1D fit scale.
   * @param {object} fitSize The fit size as {x,y}.
   * @param {object} fitOffset The fit offset as {x,y}.
   */
  this.fitToContainer = function (fitScale1D, fitSize, fitOffset) {
    // update konva
    konvaStage.setWidth(fitSize.x);
    konvaStage.setHeight(fitSize.y);

    // previous scale without fit
    var previousScale = {
      x: konvaStage.scale().x / fitScale.x,
      y: konvaStage.scale().y / fitScale.y
    };
    // update fit scale
    fitScale = {
      x: fitScale1D * baseSpacing.x,
      y: fitScale1D * baseSpacing.y
    };
    // update scale
    konvaStage.scale({
      x: previousScale.x * fitScale.x,
      y: previousScale.y * fitScale.y
    });

    // update offsets
    viewOffset = {
      x: fitOffset.x / fitScale.x,
      y: fitOffset.y / fitScale.y
    };
    konvaStage.offset({
      x: viewOffset.x + baseOffset.x + zoomOffset.x + flipOffset.x,
      y: viewOffset.y + baseOffset.y + zoomOffset.y + flipOffset.y
    });
  };

  /**
   * Check the visibility of a given group.
   *
   * @param {string} id The id of the group.
   * @returns {boolean} True if the group is visible.
   */
  this.isGroupVisible = function (id) {
    // get the group
    var group = drawController.getGroup(id);
    if (typeof group === 'undefined') {
      return false;
    }
    // get visibility
    return group.isVisible();
  };

  /**
   * Toggle the visibility of a given group.
   *
   * @param {string} id The id of the group.
   * @returns {boolean} False if the group cannot be found.
   */
  this.toogleGroupVisibility = function (id) {
    // get the group
    var group = drawController.getGroup(id);
    if (typeof group === 'undefined') {
      return false;
    }
    // toggle visible
    group.visible(!group.isVisible());

    // udpate
    this.draw();

    return true;
  };

  /**
   * Delete a Draw from the stage.
   *
   * @param {string} id The id of the group to delete.
   * @param {object} exeCallback The callback to call once the
   *  DeleteCommand has been executed.
   */
  this.deleteDraw = function (id, exeCallback) {
    drawController.deleteDraw(id, fireEvent, exeCallback);
  };

  /**
   * Delete all Draws from the stage.
   *
   * @param {object} exeCallback The callback to call once the
   *  DeleteCommand has been executed.
   */
  this.deleteDraws = function (exeCallback) {
    drawController.deleteDraws(fireEvent, exeCallback);
  };

  /**
   * Enable and listen to container interaction events.
   */
  this.bindInteraction = function () {
    konvaStage.listening(true);
    // allow pointer events
    containerDiv.style.pointerEvents = 'auto';
    // interaction events
    var names = dwv.gui.interactionEventNames;
    for (var i = 0; i < names.length; ++i) {
      containerDiv.addEventListener(names[i], fireEvent);
    }
  };

  /**
   * Disable and stop listening to container interaction events.
   */
  this.unbindInteraction = function () {
    konvaStage.listening(false);
    // disable pointer events
    containerDiv.style.pointerEvents = 'none';
    // interaction events
    var names = dwv.gui.interactionEventNames;
    for (var i = 0; i < names.length; ++i) {
      containerDiv.removeEventListener(names[i], fireEvent);
    }
  };

  /**
   * Set the current position.
   *
   * @param {dwv.math.Point} position The new position.
   * @param {dwv.math.Index} index The new index.
   * @returns {boolean} True if the position was updated.
   */
  this.setCurrentPosition = function (position, index) {
    this.getDrawController().activateDrawLayer(
      index, planeHelper.getScrollIndex());
    // TODO: add check
    return true;
  };

  /**
   * Add an event listener to this class.
   *
   * @param {string} type The event type.
   * @param {object} callback The method associated with the provided
   *   event type, will be called with the fired event.
   */
  this.addEventListener = function (type, callback) {
    listenerHandler.add(type, callback);
  };

  /**
   * Remove an event listener from this class.
   *
   * @param {string} type The event type.
   * @param {object} callback The method associated with the provided
   *   event type.
   */
  this.removeEventListener = function (type, callback) {
    listenerHandler.remove(type, callback);
  };

  /**
   * Fire an event: call all associated listeners with the input event object.
   *
   * @param {object} event The event to fire.
   * @private
   */
  function fireEvent(event) {
    event.srclayerid = self.getId();
    event.dataid = dataIndex;
    listenerHandler.fireEvent(event);
  }

  // common layer methods [end] ---------------

  /**
   * Update label scale: compensate for it so
   *   that label size stays visually the same.
   *
   * @param {object} scale The scale to compensate for as {x,y}.
   */
  function updateLabelScale(scale) {
    // same formula as in style::applyZoomScale:
    // compensate for scale and times 2 so that font 10 looks like a 10
    var ratioX = 2 / scale.x;
    var ratioY = 2 / scale.y;
    // compensate scale for labels
    var labels = konvaStage.find('Label');
    for (var i = 0; i < labels.length; ++i) {
      labels[i].scale({x: ratioX, y: ratioY});
    }
  }
}; // DrawLayer class