src_tools_protractor.js

import {Line, getAngle} from '../math/line';
import {Protractor} from '../math/protractor';
import {Point2D} from '../math/point';
import {defaults} from '../app/defaults';
import {
  getLineShape,
  DRAW_DEBUG,
  getDefaultAnchor,
  getAnchorShape
} from './drawBounds';
import {LabelFactory} from './labelFactory';

// external
import Konva from 'konva';

// doc imports
/* eslint-disable no-unused-vars */
import {Style} from '../gui/style';
import {Annotation} from '../image/annotation';
/* eslint-enable no-unused-vars */

/**
 * Protractor factory.
 */
export class ProtractorFactory {

  /**
   * The name of the factory.
   *
   * @type {string}
   */
  #name = 'protractor';

  /**
   * The associated label factory.
   *
   * @type {LabelFactory}
   */
  #labelFactory = new LabelFactory(this.#getDefaultLabelPosition);

  /**
   * Does this factory support the input math shape.
   *
   * @param {object} mathShape The mathematical shape.
   * @returns {boolean} True if supported.
   */
  static supports(mathShape) {
    return mathShape instanceof Protractor;
  }

  /**
   * Get the name of the factory.
   *
   * @returns {string} The name.
   */
  getName() {
    return this.#name;
  }

  /**
   * Get the name of the shape group.
   *
   * @returns {string} The name.
   */
  getGroupName() {
    return this.#name + '-group';
  }

  /**
   * Get the number of points needed to build the shape.
   *
   * @returns {number} The number of points.
   */
  getNPoints() {
    return 3;
  }

  /**
   * Get the timeout between point storage.
   *
   * @returns {number} The timeout in milliseconds.
   */
  getTimeout() {
    return 500;
  }

  /**
   * Set an annotation math shape from input points.
   *
   * @param {Annotation} annotation The annotation.
   * @param {Point2D[]} points The points.
   */
  setAnnotationMathShape(annotation, points) {
    annotation.mathShape = this.#calculateMathShape(points);
    annotation.setTextExpr(this.#getDefaultLabel());
    annotation.updateQuantification();
  }

  /**
   * Create a line shape to be displayed.
   *
   * @param {Annotation} annotation The associated annotation.
   * @param {Style} style The drawing style.
   * @returns {Konva.Group} The Konva group.
   */
  createShapeGroup(annotation, style) {
    const protractor = annotation.mathShape;

    // konva group
    const group = new Konva.Group();
    group.name(this.getGroupName());
    group.visible(true);
    group.id(annotation.id);
    // konva shape
    const shape = this.#createShape(annotation, style);
    group.add(this.#createShape(annotation, style));

    if (protractor.getLength() === this.getNPoints()) {
      // extras
      const extras = this.#createShapeExtras(annotation, style);
      for (const extra of extras) {
        group.add(extra);
      }
      // konva label
      const label = this.#labelFactory.create(annotation, style);
      group.add(this.#labelFactory.create(annotation, style));
      // label-shape connector
      const connectorsPos = this.#getConnectorsPositions(shape);
      group.add(this.#labelFactory.getConnector(connectorsPos, label, style));
      // konva shadow (if debug)
      if (DRAW_DEBUG) {
        group.add(this.#getDebugShadow(annotation));
      }
    }
    return group;
  }


  /**
   * Get the connectors positions for the shape.
   *
   * @param {Konva.Line} shape The associated shape.
   * @returns {Point2D[]} The connectors positions.
   */
  #getConnectorsPositions(shape) {
    const points = shape.points();
    const sx = shape.x();
    const sy = shape.y();
    return [
      new Point2D(points[2] + sx, points[3] + sy)
    ];
  }

  /**
   * Get the anchors positions for the shape.
   *
   * @param {Konva.Line} shape The associated shape.
   * @returns {Point2D[]} The anchor positions.
   */
  #getAnchorsPositions(shape) {
    const points = shape.points();
    const sx = shape.x();
    const sy = shape.y();
    return [
      new Point2D(points[0] + sx, points[1] + sy),
      new Point2D(points[2] + sx, points[3] + sy),
      new Point2D(points[4] + sx, points[5] + sy)
    ];
  }

  /**
   * Get anchors to update a line shape.
   *
   * @param {Konva.Line} shape The associated shape.
   * @param {Style} style The application style.
   * @returns {Konva.Ellipse[]} A list of anchors.
   */
  getAnchors(shape, style) {
    const positions = this.#getAnchorsPositions(shape);
    const anchors = [];
    for (let i = 0; i < positions.length; ++i) {
      anchors.push(getDefaultAnchor(
        positions[i].getX(),
        positions[i].getY(),
        'anchor' + i,
        style
      ));
    }
    return anchors;
  }

  /**
   * Constrain anchor movement.
   *
   * @param {Konva.Ellipse} _anchor The active anchor.
   */
  constrainAnchorMove(_anchor) {
    // no constraints
  }

  /**
   * Update shape and label on anchor move taking the updated
   *   annotation as input.
   *
   * @param {Annotation} annotation The associated annotation.
   * @param {Konva.Ellipse} anchor The active anchor.
   * @param {Style} style The application style.
   */
  updateShapeGroupOnAnchorMove(annotation, anchor, style) {
    // parent group
    const group = anchor.getParent();
    if (!(group instanceof Konva.Group)) {
      return;
    }

    // update shape and anchors
    this.#updateShape(annotation, anchor, style);
    // update label
    this.updateLabelContent(annotation, group, style);
    // label position
    if (typeof annotation.labelPosition === 'undefined') {
      // update label position if default position
      this.#labelFactory.updatePosition(annotation, group);
    } else {
      // update connector if not default position
      this.updateConnector(group);
    }
    // update shadow
    if (DRAW_DEBUG) {
      this.#updateDebugShadow(annotation, group);
    }
  }

  /**
   * Update an annotation on anchor move.
   *
   * @param {Annotation} annotation The annotation.
   * @param {Konva.Shape} anchor The anchor.
   */
  updateAnnotationOnAnchorMove(annotation, anchor) {
    // parent group
    const group = anchor.getParent();
    if (!(group instanceof Konva.Group)) {
      return;
    }
    // associated shape
    const kline = this.#getShape(group);
    // find special points
    const begin = getAnchorShape(group, 0);
    const mid = getAnchorShape(group, 1);
    const end = getAnchorShape(group, 2);

    // math shape
    // compensate for possible shape drag
    const pointBegin = new Point2D(
      begin.x() - kline.x(),
      begin.y() - kline.y()
    );
    const pointMid = new Point2D(
      mid.x() - kline.x(),
      mid.y() - kline.y()
    );
    const pointEnd = new Point2D(
      end.x() - kline.x(),
      end.y() - kline.y()
    );
    annotation.mathShape = new Protractor([pointBegin, pointMid, pointEnd]);
    // quantification
    annotation.updateQuantification();
  }

  /**
   * Update an annotation on translation (shape move).
   *
   * @param {Annotation} annotation The annotation.
   * @param {object} translation The translation.
   */
  updateAnnotationOnTranslation(annotation, translation) {
    // math shape
    const protractor = annotation.mathShape;
    const newPointList = [];
    for (let i = 0; i < 3; ++i) {
      newPointList.push(new Point2D(
        protractor.getPoint(i).getX() + translation.x,
        protractor.getPoint(i).getY() + translation.y
      ));
    }
    annotation.mathShape = new Protractor(newPointList);
    // quantification
    annotation.updateQuantification();
  }

  /**
   * Update the shape label.
   *
   * @param {Annotation} annotation The associated annotation.
   * @param {Konva.Group} group The shape group.
   * @param {Style} _style The application style.
   */
  updateLabelContent(annotation, group, _style) {
    this.#labelFactory.updateContent(annotation, group);
  }

  /**
   * Update the shape connector.
   *
   * @param {Konva.Group} group The shape group.
   */
  updateConnector(group) {
    const kshape = this.#getShape(group);
    const connectorsPos = this.#getConnectorsPositions(kshape);
    this.#labelFactory.updateConnector(group, connectorsPos);
  }

  /**
   * Calculate the mathematical shape from a list of points.
   *
   * @param {Point2D[]} points The points that define the shape.
   * @returns {Protractor} The mathematical shape.
   */
  #calculateMathShape(points) {
    return new Protractor(points);
  }

  /**
   * Get the default labels.
   *
   * @returns {object} The label list.
   */
  #getDefaultLabel() {
    return defaults.labelText.protractor;
  }

  /**
   * Creates the konva shape.
   *
   * @param {Annotation} annotation The associated annotation.
   * @param {Style} style The drawing style.
   * @returns {Konva.Line} The konva shape.
   */
  #createShape(annotation, style) {
    const protractor = annotation.mathShape;
    const points = [];
    for (let i = 0; i < protractor.getLength(); ++i) {
      points.push(protractor.getPoint(i).getX());
      points.push(protractor.getPoint(i).getY());
    }

    // konva line
    const kshape = new Konva.Line({
      points: points,
      stroke: annotation.colour,
      strokeWidth: style.getStrokeWidth(),
      strokeScaleEnabled: false,
      name: 'shape'
    });

    if (protractor.getLength() === this.getNPoints()) {
      // larger hitfunc
      kshape.hitFunc(function (context) {
        context.beginPath();
        context.moveTo(
          protractor.getPoint(0).getX(), protractor.getPoint(0).getY());
        context.lineTo(
          protractor.getPoint(1).getX(), protractor.getPoint(1).getY());
        context.lineTo(
          protractor.getPoint(2).getX(), protractor.getPoint(2).getY());
        context.closePath();
        context.fillStrokeShape(kshape);
      });
    }

    return kshape;
  }

  /**
   * Get the associated shape from a group.
   *
   * @param {Konva.Group} group The group to look into.
   * @returns {Konva.Line|undefined} The shape.
   */
  #getShape(group) {
    return getLineShape(group);
  }

  /**
   * Creates the konva shape extras.
   *
   * @param {Annotation} annotation The associated annotation.
   * @param {Style} style The drawing style.
   * @returns {Array} The konva shape extras.
   */
  #createShapeExtras(annotation, style) {
    const protractor = annotation.mathShape;
    const line0 = new Line(
      protractor.getPoint(0), protractor.getPoint(1));
    const line1 = new Line(
      protractor.getPoint(1), protractor.getPoint(2));

    let angle = getAngle(line0, line1);
    let inclination = line0.getInclination();
    if (angle > 180) {
      angle = 360 - angle;
      inclination += angle;
    }

    const radius = Math.min(line0.getLength(), line1.getLength()) * 33 / 100;
    const karc = new Konva.Arc({
      innerRadius: radius,
      outerRadius: radius,
      stroke: annotation.colour,
      strokeWidth: style.getStrokeWidth(),
      strokeScaleEnabled: false,
      angle: angle,
      rotation: -inclination,
      x: protractor.getPoint(1).getX(),
      y: protractor.getPoint(1).getY(),
      name: 'shape-arc'
    });

    return [karc];
  }

  /**
   * Get the default annotation label position.
   *
   * @param {Annotation} annotation The annotation.
   * @returns {Point2D} The position.
   */
  #getDefaultLabelPosition(annotation) {
    const protractor = annotation.mathShape;
    const line0 = new Line(
      protractor.getPoint(0), protractor.getPoint(1));
    const line1 = new Line(
      protractor.getPoint(1), protractor.getPoint(2));

    const midX =
      (line0.getMidpoint().getX() + line1.getMidpoint().getX()) / 2;
    const midY =
      (line0.getMidpoint().getY() + line1.getMidpoint().getY()) / 2;

    return new Point2D(
      midX,
      midY
    );
  }

  /**
   * Update shape and label on anchor move taking the updated
   *   annotation as input.
   *
   * @param {Annotation} annotation The associated annotation.
   * @param {Konva.Ellipse} anchor The active anchor.
   * @param {Style} _style The application style.
   */
  #updateShape(annotation, anchor, _style) {
    const protractor = annotation.mathShape;
    const line0 = new Line(
      protractor.getPoint(0), protractor.getPoint(1));
    const line1 = new Line(
      protractor.getPoint(1), protractor.getPoint(2));

    // parent group
    const group = anchor.getParent();
    if (!(group instanceof Konva.Group)) {
      return;
    }
    // associated shape
    const kline = this.#getShape(group);

    // reset position after possible shape drag
    kline.position({x: 0, y: 0});
    // update shape
    kline.points([
      protractor.getPoint(0).getX(),
      protractor.getPoint(0).getY(),
      protractor.getPoint(1).getX(),
      protractor.getPoint(1).getY(),
      protractor.getPoint(2).getX(),
      protractor.getPoint(2).getY()
    ]);

    // associated arc
    const karc = group.getChildren(function (node) {
      return node.name() === 'shape-arc';
    })[0];
    if (!(karc instanceof Konva.Arc)) {
      return;
    }

    // find special points
    const begin = getAnchorShape(group, 0);
    const mid = getAnchorShape(group, 1);
    const end = getAnchorShape(group, 2);

    // update special points
    switch (anchor.id()) {
    case 'anchor0':
      begin.x(anchor.x());
      begin.y(anchor.y());
      break;
    case 'anchor1':
      mid.x(anchor.x());
      mid.y(anchor.y());
      break;
    case 'anchor2':
      end.x(anchor.x());
      end.y(anchor.y());
      break;
    }

    // angle
    let angle = getAngle(line0, line1);
    let inclination = line0.getInclination();
    if (angle > 180) {
      angle = 360 - angle;
      inclination += angle;
    }

    // arc
    const radius = Math.min(line0.getLength(), line1.getLength()) * 33 / 100;
    karc.innerRadius(radius);
    karc.outerRadius(radius);
    karc.angle(angle);
    karc.rotation(-inclination);
    const arcPos = {x: mid.x(), y: mid.y()};
    karc.position(arcPos);

    // larger hitfunc
    kline.hitFunc(function (context) {
      context.beginPath();
      context.moveTo(
        protractor.getPoint(0).getX(), protractor.getPoint(0).getY());
      context.lineTo(
        protractor.getPoint(1).getX(), protractor.getPoint(1).getY());
      context.lineTo(
        protractor.getPoint(2).getX(), protractor.getPoint(2).getY());
      context.closePath();
      context.fillStrokeShape(kline);
    });
  }

  /**
   * Get the debug shadow.
   *
   * @param {Annotation} _annotation The annotation to shadow.
   * @param {Konva.Group} [_group] The associated group.
   * @returns {Konva.Group|undefined} The shadow konva group.
   */
  #getDebugShadow(_annotation, _group) {
    return;
  }

  /**
   * Update the debug shadow.
   *
   * @param {Annotation} _annotation The annotation to shadow.
   * @param {Konva.Group} _group The associated group.
   */
  #updateDebugShadow(_annotation, _group) {
    // does nothing
  }

} // class ProtractorFactory