src_tools_circle.js

import {Circle} from '../math/circle';
import {Point2D} from '../math/point';
import {getFlags, replaceFlags} from '../utils/string';
import {logger} from '../utils/logger';
import {defaults} from '../app/defaults';
import {getDefaultAnchor} from './editor';
import {DRAW_DEBUG} from './draw';
// external
import Konva from 'konva';

// doc imports
/* eslint-disable no-unused-vars */
import {ViewController} from '../app/viewController';
import {Style} from '../gui/style';
/* eslint-enable no-unused-vars */

/**
 * Circle factory.
 */
export class CircleFactory {
  /**
   * Get the name of the shape group.
   *
   * @returns {string} The name.
   */
  getGroupName() {
    return 'circle-group';
  }

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

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

  /**
   * Is the input group a group of this factory?
   *
   * @param {Konva.Group} group The group to test.
   * @returns {boolean} True if the group is from this fcatory.
   */
  isFactoryGroup(group) {
    return this.getGroupName() === group.name();
  }

  /**
   * Calculates the mathematical circle.
   *
   * @param {Point2D[]} points The points that define the circle.
   * @returns {Circle} The mathematical circle.
   */
  #calculateMathShape(points) {
    // calculate radius
    const a = Math.abs(points[0].getX() - points[1].getX());
    const b = Math.abs(points[0].getY() - points[1].getY());
    const radius = Math.round(Math.sqrt(a * a + b * b));
    // physical shape
    return new Circle(points[0], radius);
  }

  /**
   * Creates the konva circle shape.
   *
   * @param {Circle} circle The mathematical circle.
   * @param {Style} style The drawing style.
   * @returns {Konva.Circle} The konva circle shape.
   */
  #createShape(circle, style) {
    return new Konva.Circle({
      x: circle.getCenter().getX(),
      y: circle.getCenter().getY(),
      radius: circle.getRadius(),
      stroke: style.getLineColour(),
      strokeWidth: style.getStrokeWidth(),
      strokeScaleEnabled: false,
      name: 'shape'
    });
  }

  /**
   * Creates the konva label.
   *
   * @param {Circle} circle The mathematical circle.
   * @param {Style} style The drawing style.
   * @param {ViewController} viewController The associated view controller.
   * @returns {Konva.Label} The Konva label.
   */
  #createLabel(circle, style, viewController) {
    // quantification
    const ktext = new Konva.Text({
      fontSize: style.getFontSize(),
      fontFamily: style.getFontFamily(),
      fill: style.getLineColour(),
      padding: style.getTextPadding(),
      shadowColor: style.getShadowLineColour(),
      shadowOffset: style.getShadowOffset(),
      name: 'text'
    });
    let textExpr = '';
    const modality = viewController.getModality();
    if (typeof defaults.labelText.circle[modality] !== 'undefined') {
      textExpr = defaults.labelText.circle[modality];
    } else {
      textExpr = defaults.labelText.circle['*'];
    }
    const quant = circle.quantify(
      viewController,
      getFlags(textExpr));
    ktext.setText(replaceFlags(textExpr, quant));
    // augment text with meta data
    // @ts-expect-error
    ktext.meta = {
      textExpr: textExpr,
      quantification: quant
    };
    // label
    const klabel = new Konva.Label({
      x: circle.getCenter().getX() - circle.getRadius(),
      y: circle.getCenter().getY() + circle.getRadius(),
      scale: style.applyZoomScale(1),
      visible: textExpr.length !== 0,
      name: 'label'
    });
    klabel.add(ktext);
    klabel.add(new Konva.Tag({
      fill: style.getLineColour(),
      opacity: style.getTagOpacity()
    }));

    return klabel;
  }

  /**
   * Create a circle shape to be displayed.
   *
   * @param {Point2D[]} points The points from which to extract the circle.
   * @param {Style} style The drawing style.
   * @param {ViewController} viewController The associated view controller.
   * @returns {Konva.Group} The Konva group.
   */
  create(points, style, viewController) {
    // Create group
    const group = new Konva.Group();
    group.name(this.getGroupName());
    group.visible(true);

    // Create and add shape
    const mathShape = this.#calculateMathShape(points);
    const kShape = this.#createShape(mathShape, style);
    group.add(kShape);
    // Create and add label
    const kLabel = this.#createLabel(mathShape, style, viewController);
    group.add(kLabel);
    // Add shadow (if debug)
    let kshadow;
    if (DRAW_DEBUG) {
      kshadow = this.#getShadowCircle(mathShape);
      group.add(kshadow);
    }

    return group;
  }

  /**
   * Get anchors to update a circle shape.
   *
   * @param {Konva.Circle} shape The associated shape.
   * @param {Style} style The application style.
   * @returns {Konva.Ellipse[]} A list of anchors.
   */
  getAnchors(shape, style) {
    const centerX = shape.x();
    const centerY = shape.y();
    const radius = shape.radius();

    const anchors = [];
    anchors.push(getDefaultAnchor(
      centerX - radius, centerY, 'left', style
    ));
    anchors.push(getDefaultAnchor(
      centerX + radius, centerY, 'right', style
    ));
    anchors.push(getDefaultAnchor(
      centerX, centerY + radius, 'bottom', style
    ));
    anchors.push(getDefaultAnchor(
      centerX, centerY - radius, 'top', style
    ));
    return anchors;
  }

  /**
   * Update a circle shape.
   *
   * @param {Konva.Ellipse} anchor The active anchor.
   * @param {Style} _style The app style.
   * @param {ViewController} viewController The associated view controller.
   */
  update(anchor, _style, viewController) {
    // parent group
    const group = anchor.getParent();
    if (!(group instanceof Konva.Group)) {
      return;
    }
    // associated shape
    const kcircle = group.getChildren(function (node) {
      return node.name() === 'shape';
    })[0];
    if (!(kcircle instanceof Konva.Circle)) {
      return;
    }
    // associated label
    const klabel = group.getChildren(function (node) {
      return node.name() === 'label';
    })[0];
    // find special points
    const left = group.getChildren(function (node) {
      return node.id() === 'left';
    })[0];
    const right = group.getChildren(function (node) {
      return node.id() === 'right';
    })[0];
    const bottom = group.getChildren(function (node) {
      return node.id() === 'bottom';
    })[0];
    const top = group.getChildren(function (node) {
      return node.id() === 'top';
    })[0];
    // debug shadow
    let kshadow;
    if (DRAW_DEBUG) {
      kshadow = group.getChildren(function (node) {
        return node.name() === 'shadow';
      })[0];
    }

    // circle center
    const center = {
      x: kcircle.x(),
      y: kcircle.y()
    };

    let radius;

    // update 'self' (undo case) and special points
    switch (anchor.id()) {
    case 'left':
      radius = center.x - anchor.x();
      // update self (while blocking y)
      left.x(anchor.x());
      left.y(right.y());
      // update others
      right.x(center.x + radius);
      bottom.y(center.y + radius);
      top.y(center.y - radius);
      break;
    case 'right':
      radius = anchor.x() - center.x;
      // update self (while blocking y)
      right.x(anchor.x());
      right.y(left.y());
      // update others
      left.x(center.x - radius);
      bottom.y(center.y + radius);
      top.y(center.y - radius);
      break;
    case 'bottom':
      radius = anchor.y() - center.y;
      // update self (while blocking x)
      bottom.x(top.x());
      bottom.y(anchor.y());
      // update others
      left.x(center.x - radius);
      right.x(center.x + radius);
      top.y(center.y - radius);
      break;
    case 'top':
      radius = center.y - anchor.y();
      // update self (while blocking x)
      top.x(bottom.x());
      top.y(anchor.y());
      // update others
      left.x(center.x - radius);
      right.x(center.x + radius);
      bottom.y(center.y + radius);
      break;
    default :
      logger.error('Unhandled anchor id: ' + anchor.id());
      break;
    }

    // update shape: just update the radius
    kcircle.radius(Math.abs(radius));
    // new circle
    const centerPoint = new Point2D(
      group.x() + center.x,
      group.y() + center.y
    );
    const circle = new Circle(centerPoint, radius);

    // debug shadow
    if (kshadow) {
      // remove previous
      kshadow.destroy();
      // add new
      group.add(this.#getShadowCircle(circle, group));
    }

    // update label position
    const textPos = {
      x: center.x - Math.abs(radius),
      y: center.y + Math.abs(radius)
    };
    klabel.position(textPos);

    // update quantification
    this.#updateCircleQuantification(group, viewController);
  }

  /**
   * Update the quantification of a Circle.
   *
   * @param {Konva.Group} group The group with the shape.
   * @param {ViewController} viewController The associated view controller.
   */
  updateQuantification(group, viewController) {
    this.#updateCircleQuantification(group, viewController);
  }

  /**
   * Update the quantification of a Circle (as a static
   *   function to be used in update).
   *
   * @param {Konva.Group} group The group with the shape.
   * @param {ViewController} viewController The associated view controller.
   */
  #updateCircleQuantification(
    group, viewController) {
    // associated shape
    const kcircle = group.getChildren(function (node) {
      return node.name() === 'shape';
    })[0];
    if (!(kcircle instanceof Konva.Circle)) {
      return;
    }
    // associated label
    const klabel = group.getChildren(function (node) {
      return node.name() === 'label';
    })[0];
    if (!(klabel instanceof Konva.Label)) {
      return;
    }

    // positions: add possible group offset
    const centerPoint = new Point2D(
      group.x() + kcircle.x(),
      group.y() + kcircle.y()
    );
    // circle
    const circle = new Circle(centerPoint, kcircle.radius());

    // update text
    const ktext = klabel.getText();
    // @ts-expect-error
    const meta = ktext.meta;
    const quantification = circle.quantify(
      viewController,
      getFlags(meta.textExpr));
    ktext.setText(replaceFlags(meta.textExpr, quantification));
    // update meta
    meta.quantification = quantification;
  }

  /**
   * Get the debug shadow.
   *
   * @param {Circle} circle The circle to shadow.
   * @param {Konva.Group} [group] The associated group.
   * @returns {Konva.Group} The shadow konva group.
   */
  #getShadowCircle(circle, group) {
    // possible group offset
    let offsetX = 0;
    let offsetY = 0;
    if (typeof group !== 'undefined') {
      offsetX = group.x();
      offsetY = group.y();
    }
    const kshadow = new Konva.Group();
    kshadow.name('shadow');
    const regions = circle.getRound();
    for (let i = 0; i < regions.length; ++i) {
      const region = regions[i];
      const minX = region[0][0];
      const minY = region[0][1];
      const maxX = region[1][0];
      const pixelLine = new Konva.Rect({
        x: minX - offsetX,
        y: minY - offsetY,
        width: maxX - minX,
        height: 1,
        fill: 'grey',
        strokeWidth: 0,
        strokeScaleEnabled: false,
        opacity: 0.3,
        name: 'shadow-element'
      });
      kshadow.add(pixelLine);
    }
    return kshadow;
  }

} // class CircleFactory