src_tools_drawShapeEditor.js

import {logger} from '../utils/logger';
import {UpdateAnnotationCommand} from './drawCommands';
import {validateAnchorPosition} from './drawBounds';
// external
import Konva from 'konva';

// doc imports
/* eslint-disable no-unused-vars */
import {App} from '../app/application';
import {DrawLayer} from '../gui/drawLayer';
import {Annotation} from '../image/annotation';
/* eslint-enable no-unused-vars */

/**
 * Draw shape editor.
 */
export class DrawShapeEditor {

  /**
   * Associated app.
   *
   * @type {App}
   */
  #app;

  /**
   * Event callback.
   *
   * @type {Function}
   */
  #eventCallback;

  /**
   * @param {App} app The associated application.
   * @param {Function} eventCallback Event callback.
   */
  constructor(app, eventCallback) {
    this.#app = app;
    this.#eventCallback = eventCallback;
  }

  /**
   * Current shape factory.
   *
   * @type {object}
   */
  #currentFactory = null;

  /**
   * Edited shape.
   *
   * @type {Konva.Shape}
   */
  #shape = null;

  /**
   * Associated draw layer. Used to bound anchor move.
   *
   * @type {DrawLayer}
   */
  #drawLayer;

  /**
   * The associated annotation.
   *
   * @type {Annotation}
   */
  #annotation;

  /**
   * Active flag.
   *
   * @type {boolean}
   */
  #isActive = false;

  /**
   * @callback eventFn
   * @param {object} event The event.
   */

  /**
   * Set the shape to edit.
   *
   * @param {Konva.Shape} inshape The shape to edit.
   * @param {DrawLayer} drawLayer The associated draw layer.
   * @param {Annotation} annotation The associated annotation.
   */
  setShape(inshape, drawLayer, annotation) {
    this.#shape = inshape;
    this.#drawLayer = drawLayer;
    this.#annotation = annotation;

    if (this.#shape) {
      // remove old anchors
      this.#removeAnchors();

      this.#currentFactory = annotation.getFactory();
      if (this.#currentFactory === null) {
        throw new Error('Could not find a factory to update shape.');
      }

      // add new anchors
      this.#addAnchors();
    }
  }

  /**
   * Get the edited shape.
   *
   * @returns {Konva.Shape} The edited shape.
   */
  getShape() {
    return this.#shape;
  }

  /**
   * Get the edited annotation.
   *
   * @returns {Annotation} The annotation.
   */
  getAnnotation() {
    return this.#annotation;
  }

  /**
   * Get the active flag.
   *
   * @returns {boolean} The active flag.
   */
  isActive() {
    return this.#isActive;
  }

  /**
   * Enable the editor. Redraws the layer.
   */
  enable() {
    this.#isActive = true;
    if (this.#shape) {
      this.#setAnchorsVisible(true);
      if (this.#shape.getLayer()) {
        this.#shape.getLayer().draw();
      }
    }
  }

  /**
   * Disable the editor. Redraws the layer.
   */
  disable() {
    this.#isActive = false;
    if (this.#shape) {
      this.#setAnchorsVisible(false);
      if (this.#shape.getLayer()) {
        this.#shape.getLayer().draw();
      }
    }
  }

  /**
   * Reset the editor.
   */
  reset() {
    this.#shape = undefined;
    this.#drawLayer = undefined;
    this.#annotation = undefined;
  }

  /**
   * Reset the anchors.
   */
  resetAnchors() {
    // remove previous controls
    this.#removeAnchors();
    // add anchors
    this.#addAnchors();
    // set them visible
    this.#setAnchorsVisible(true);
  }

  /**
   * Apply a function on all anchors.
   *
   * @param {object} func A f(shape) function.
   */
  #applyFuncToAnchors(func) {
    if (this.#shape && this.#shape.getParent()) {
      const anchors = this.#shape.getParent().find('.anchor');
      anchors.forEach(func);
    }
  }

  /**
   * Set anchors visibility.
   *
   * @param {boolean} flag The visible flag.
   */
  #setAnchorsVisible(flag) {
    this.#applyFuncToAnchors(function (anchor) {
      anchor.visible(flag);
    });
  }

  /**
   * Set anchors active.
   *
   * @param {boolean} flag The active (on/off) flag.
   */
  setAnchorsActive(flag) {
    let func = null;
    if (flag) {
      func = (anchor) => {
        this.#setAnchorOn(anchor);
      };
    } else {
      func = (anchor) => {
        this.#setAnchorOff(anchor);
      };
    }
    this.#applyFuncToAnchors(func);
  }

  /**
   * Remove anchors.
   */
  #removeAnchors() {
    this.#applyFuncToAnchors(function (anchor) {
      anchor.remove();
    });
  }

  /**
   * Add the shape anchors.
   */
  #addAnchors() {
    // exit if no shape or no layer
    if (!this.#shape || !this.#shape.getLayer()) {
      return;
    }
    // get shape group
    const group = this.#shape.getParent();

    // activate and add anchors to group
    const anchors =
      this.#currentFactory.getAnchors(this.#shape, this.#app.getStyle());
    for (let i = 0; i < anchors.length; ++i) {
      // set anchor on
      this.#setAnchorOn(anchors[i]);
      // add the anchor to the group
      group.add(anchors[i]);
    }
  }

  /**
   * Set the anchor on listeners.
   *
   * @param {Konva.Ellipse} anchor The anchor to set on.
   */
  #setAnchorOn(anchor) {
    let originalProps;

    // drag start listener
    anchor.on('dragstart.edit', (event) => {
      // prevent bubbling upwards
      event.cancelBubble = true;
      // store original properties
      originalProps = {
        mathShape: this.#annotation.mathShape,
        referencePoints: this.#annotation.referencePoints
      };
    });
    // drag move listener
    anchor.on('dragmove.edit', (event) => {
      const anchor = event.target;
      if (!(anchor instanceof Konva.Shape)) {
        return;
      }
      // validate the anchor position
      validateAnchorPosition(this.#drawLayer.getBaseSize(), anchor);
      if (typeof this.#currentFactory.constrainAnchorMove !== 'undefined') {
        this.#currentFactory.constrainAnchorMove(anchor);
      }

      // udpate annotation
      this.#currentFactory.updateAnnotationOnAnchorMove(
        this.#annotation, anchor);
      // udpate shape
      this.#currentFactory.updateShapeGroupOnAnchorMove(
        this.#annotation, anchor, this.#app.getStyle());

      // redraw
      if (anchor.getLayer()) {
        anchor.getLayer().draw();
      } else {
        logger.warn('No layer to draw the anchor!');
      }
      // prevent bubbling upwards
      event.cancelBubble = true;
    });
    // drag end listener
    anchor.on('dragend.edit', (event) => {
      // update annotation command
      const newProps = {
        mathShape: this.#annotation.mathShape,
        referencePoints: this.#annotation.referencePoints
      };
      const command = new UpdateAnnotationCommand(
        this.#annotation,
        originalProps,
        newProps,
        this.#drawLayer.getDrawController()
      );
      // add command to undo stack
      this.#app.addToUndoStack(command);
      // fire event manually since command is not executed
      this.#eventCallback({
        type: 'annotationupdate',
        data: this.#annotation,
        dataid: this.#drawLayer.getDataId(),
        keys: Object.keys(newProps)
      });
      // update original properties
      originalProps = {
        mathShape: newProps.mathShape,
        referencePoints: newProps.referencePoints
      };

      // prevent bubbling upwards
      event.cancelBubble = true;
    });
    // mouse down listener
    anchor.on('mousedown touchstart', (event) => {
      const anchor = event.target;
      anchor.moveToTop();
    });
    // mouse over styling
    anchor.on('mouseover.edit', (event) => {
      const anchor = event.target;
      if (!(anchor instanceof Konva.Shape)) {
        return;
      }
      // style is handled by the group
      anchor.stroke('#ddd');
      if (anchor.getLayer()) {
        anchor.getLayer().draw();
      } else {
        logger.warn('No layer to draw the anchor!');
      }
    });
    // mouse out styling
    anchor.on('mouseout.edit', (event) => {
      const anchor = event.target;
      if (!(anchor instanceof Konva.Shape)) {
        return;
      }
      // style is handled by the group
      anchor.stroke('#999');
      if (anchor.getLayer()) {
        anchor.getLayer().draw();
      } else {
        logger.warn('No layer to draw the anchor!');
      }
    });
  }

  /**
   * Set the anchor off listeners.
   *
   * @param {Konva.Ellipse} anchor The anchor to set off.
   */
  #setAnchorOff(anchor) {
    anchor.off('dragstart.edit');
    anchor.off('dragmove.edit');
    anchor.off('dragend.edit');
    anchor.off('mousedown touchstart');
    anchor.off('mouseover.edit');
    anchor.off('mouseout.edit');
  }

} // class Editor