src/tools/ellipse.js

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

/**
 * Default draw label text.
 */
dwv.tool.draw.defaultEllipseLabelText = '{surface}';

/**
 * Ellipse factory.
 *
 * @class
 */
dwv.tool.draw.EllipseFactory = function () {
  /**
   * Get the name of the shape group.
   *
   * @returns {string} The name.
   */
  this.getGroupName = function () {
    return 'ellipse-group';
  };
  /**
   * Get the number of points needed to build the shape.
   *
   * @returns {number} The number of points.
   */
  this.getNPoints = function () {
    return 2;
  };
  /**
   * Get the timeout between point storage.
   *
   * @returns {number} The timeout in milliseconds.
   */
  this.getTimeout = function () {
    return 0;
  };
};

/**
 * Is the input group a group of this factory?
 *
 * @param {object} group The group to test.
 * @returns {boolean} True if the group is from this fcatory.
 */
dwv.tool.draw.EllipseFactory.prototype.isFactoryGroup = function (group) {
  return this.getGroupName() === group.name();
};

/**
 * Create an ellipse shape to be displayed.
 *
 * @param {Array} points The points from which to extract the ellipse.
 * @param {object} style The drawing style.
 * @param {object} viewController The associated view controller.
 * @returns {object} The Konva group.
 */
dwv.tool.draw.EllipseFactory.prototype.create = function (
  points, style, viewController) {
  // calculate radius
  var a = Math.abs(points[0].getX() - points[1].getX());
  var b = Math.abs(points[0].getY() - points[1].getY());
  // physical shape
  var ellipse = new dwv.math.Ellipse(points[0], a, b);
  // draw shape
  var kshape = new Konva.Ellipse({
    x: ellipse.getCenter().getX(),
    y: ellipse.getCenter().getY(),
    radius: {x: ellipse.getA(), y: ellipse.getB()},
    stroke: style.getLineColour(),
    strokeWidth: style.getStrokeWidth(),
    strokeScaleEnabled: false,
    name: 'shape'
  });
  // quantification
  var ktext = new Konva.Text({
    fontSize: style.getFontSize(),
    fontFamily: style.getFontFamily(),
    fill: style.getLineColour(),
    padding: style.getTextPadding(),
    shadowColor: style.getShadowLineColour(),
    shadowOffset: style.getShadowOffset(),
    name: 'text'
  });
  var textExpr = '';
  if (typeof dwv.tool.draw.ellipseLabelText !== 'undefined') {
    textExpr = dwv.tool.draw.ellipseLabelText;
  } else {
    textExpr = dwv.tool.draw.defaultEllipseLabelText;
  }
  var quant = ellipse.quantify(
    viewController,
    dwv.utils.getFlags(textExpr));
  ktext.setText(dwv.utils.replaceFlags(textExpr, quant));
  // meta data
  ktext.meta = {
    textExpr: textExpr,
    quantification: quant
  };
  // label
  var klabel = new Konva.Label({
    x: ellipse.getCenter().getX(),
    y: ellipse.getCenter().getY(),
    scale: style.applyZoomScale(1),
    visible: textExpr.length !== 0,
    name: 'label'
  });
  klabel.add(ktext);
  klabel.add(new Konva.Tag({
    fill: style.getLineColour(),
    opacity: style.getTagOpacity()
  }));

  // debug shadow
  var kshadow;
  if (dwv.tool.draw.debug) {
    kshadow = dwv.tool.draw.getShadowEllipse(ellipse);
  }

  // return group
  var group = new Konva.Group();
  group.name(this.getGroupName());
  if (kshadow) {
    group.add(kshadow);
  }
  group.add(klabel);
  group.add(kshape);
  group.visible(true); // dont inherit
  return group;
};

/**
 * Get anchors to update an ellipse shape.
 *
 * @param {object} shape The associated shape.
 * @param {object} style The application style.
 * @returns {Array} A list of anchors.
 */
dwv.tool.draw.EllipseFactory.prototype.getAnchors = function (shape, style) {
  var ellipseX = shape.x();
  var ellipseY = shape.y();
  var radius = shape.radius();

  var anchors = [];
  anchors.push(dwv.tool.draw.getDefaultAnchor(
    ellipseX - radius.x, ellipseY - radius.y, 'topLeft', style
  ));
  anchors.push(dwv.tool.draw.getDefaultAnchor(
    ellipseX + radius.x, ellipseY - radius.y, 'topRight', style
  ));
  anchors.push(dwv.tool.draw.getDefaultAnchor(
    ellipseX + radius.x, ellipseY + radius.y, 'bottomRight', style
  ));
  anchors.push(dwv.tool.draw.getDefaultAnchor(
    ellipseX - radius.x, ellipseY + radius.y, 'bottomLeft', style
  ));
  return anchors;
};

/**
 * Update an ellipse shape.
 * Warning: do NOT use 'this' here, this method is passed
 *   as is to the change command.
 *
 * @param {object} anchor The active anchor.
 * @param {object} _style The app style.
 * @param {object} viewController The associated view controller.
 */
dwv.tool.draw.EllipseFactory.prototype.update = function (
  anchor, _style, viewController) {
  // parent group
  var group = anchor.getParent();
  // associated shape
  var kellipse = group.getChildren(function (node) {
    return node.name() === 'shape';
  })[0];
    // associated label
  var klabel = group.getChildren(function (node) {
    return node.name() === 'label';
  })[0];
    // find special points
  var topLeft = group.getChildren(function (node) {
    return node.id() === 'topLeft';
  })[0];
  var topRight = group.getChildren(function (node) {
    return node.id() === 'topRight';
  })[0];
  var bottomRight = group.getChildren(function (node) {
    return node.id() === 'bottomRight';
  })[0];
  var bottomLeft = group.getChildren(function (node) {
    return node.id() === 'bottomLeft';
  })[0];
  // debug shadow
  var kshadow;
  if (dwv.tool.draw.debug) {
    kshadow = group.getChildren(function (node) {
      return node.name() === 'shadow';
    })[0];
  }

  // update 'self' (undo case) and special points
  switch (anchor.id()) {
  case 'topLeft':
    topLeft.x(anchor.x());
    topLeft.y(anchor.y());
    topRight.y(anchor.y());
    bottomLeft.x(anchor.x());
    break;
  case 'topRight':
    topRight.x(anchor.x());
    topRight.y(anchor.y());
    topLeft.y(anchor.y());
    bottomRight.x(anchor.x());
    break;
  case 'bottomRight':
    bottomRight.x(anchor.x());
    bottomRight.y(anchor.y());
    bottomLeft.y(anchor.y());
    topRight.x(anchor.x());
    break;
  case 'bottomLeft':
    bottomLeft.x(anchor.x());
    bottomLeft.y(anchor.y());
    bottomRight.y(anchor.y());
    topLeft.x(anchor.x());
    break;
  default :
    dwv.logger.error('Unhandled anchor id: ' + anchor.id());
    break;
  }
  // update shape
  var radiusX = (topRight.x() - topLeft.x()) / 2;
  var radiusY = (bottomRight.y() - topRight.y()) / 2;
  var center = {
    x: topLeft.x() + radiusX,
    y: topRight.y() + radiusY
  };
  kellipse.position(center);
  var radiusAbs = {x: Math.abs(radiusX), y: Math.abs(radiusY)};
  if (radiusAbs) {
    kellipse.radius(radiusAbs);
  }
  // new ellipse
  var centerPoint = new dwv.math.Point2D(
    group.x() + center.x,
    group.y() + center.y
  );
  var ellipse = new dwv.math.Ellipse(centerPoint, radiusAbs.x, radiusAbs.y);

  // debug shadow
  if (kshadow) {
    // remove previous
    kshadow.destroy();
    // add new
    group.add(dwv.tool.draw.getShadowEllipse(ellipse, group));
  }

  // update label position
  var textPos = {x: center.x, y: center.y};
  klabel.position(textPos);

  // update quantification
  dwv.tool.draw.updateEllipseQuantification(group, viewController);
};

/**
 * Update the quantification of an Ellipse.
 *
 * @param {object} group The group with the shape.
 * @param {object} viewController The associated view controller.
 */
dwv.tool.draw.EllipseFactory.prototype.updateQuantification = function (
  group, viewController) {
  dwv.tool.draw.updateEllipseQuantification(group, viewController);
};

/**
 * Update the quantification of an Ellipse (as a static
 *   function to be used in update).
 *
 * @param {object} group The group with the shape.
 * @param {object} viewController The associated view controller.
 */
dwv.tool.draw.updateEllipseQuantification = function (
  group, viewController) {
  // associated shape
  var kellipse = group.getChildren(function (node) {
    return node.name() === 'shape';
  })[0];
  // associated label
  var klabel = group.getChildren(function (node) {
    return node.name() === 'label';
  })[0];

  // positions: add possible group offset
  var centerPoint = new dwv.math.Point2D(
    group.x() + kellipse.x(),
    group.y() + kellipse.y()
  );
  // circle
  var ellipse = new dwv.math.Ellipse(
    centerPoint, kellipse.radius().x, kellipse.radius().y);

  // update text
  var ktext = klabel.getText();
  var quantification = ellipse.quantify(
    viewController,
    dwv.utils.getFlags(ktext.meta.textExpr));
  ktext.setText(dwv.utils.replaceFlags(ktext.meta.textExpr, quantification));
  // update meta
  ktext.meta.quantification = quantification;
};

/**
 * Get the debug shadow.
 *
 * @param {dwv.math.Ellipse} ellipse The ellipse to shadow.
 * @param {object} group The associated group.
 * @returns {object} The shadow konva group.
 */
dwv.tool.draw.getShadowEllipse = function (ellipse, group) {
  // possible group offset
  var offsetX = 0;
  var offsetY = 0;
  if (typeof group !== 'undefined') {
    offsetX = group.x();
    offsetY = group.y();
  }
  var kshadow = new Konva.Group();
  kshadow.name('shadow');
  var regions = ellipse.getRound();
  for (var i = 0; i < regions.length; ++i) {
    var region = regions[i];
    var minX = region[0][0];
    var minY = region[0][1];
    var maxX = region[1][0];
    var 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;
};