src/tools/draw.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 || {};

/**
 * Debug flag.
 */
dwv.tool.draw.debug = false;

/**
 * Drawing tool.
 *
 * This tool is responsible for the draw layer group structure. The layout is:
 *
 * drawLayer
 * |_ positionGroup: name="position-group", id="#2-0#_#3-1""
 *    |_ shapeGroup: name="{shape name}-group", id="#"
 *       |_ shape: name="shape"
 *       |_ label: name="label"
 *       |_ extra: line tick, protractor arc...
 *
 * Discussion:
 * - posGroup > shapeGroup
 *    pro: slice/frame display: 1 loop
 *    cons: multi-slice shape splitted in positionGroups
 * - shapeGroup > posGroup
 *    pros: more logical
 *    cons: slice/frame display: 2 loops
 *
 * @class
 * @param {dwv.App} app The associated application.
 */
dwv.tool.Draw = function (app) {
  /**
   * Closure to self: to be used by event handlers.
   *
   * @private
   * @type {dwv.tool.Draw }
   */
  var self = this;
  /**
   * Interaction start flag.
   *
   * @private
   * @type {boolean}
   */
  var started = false;

  /**
   * Shape factory list
   *
   * @type {object}
   */
  this.shapeFactoryList = null;

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

  /**
   * Draw command.
   *
   * @private
   * @type {object}
   */
  var command = null;
  /**
   * Current shape group.
   *
   * @private
   * @type {object}
   */
  var tmpShapeGroup = null;

  /**
   * Shape name.
   *
   * @type {string}
   */
  this.shapeName = 0;

  /**
   * List of points.
   *
   * @private
   * @type {Array}
   */
  var points = [];

  /**
   * Last selected point.
   *
   * @private
   * @type {object}
   */
  var lastPoint = null;

  /**
   * Shape editor.
   *
   * @private
   * @type {object}
   */
  var shapeEditor = new dwv.tool.ShapeEditor(app);

  // associate the event listeners of the editor
  //  with those of the draw tool
  shapeEditor.setDrawEventCallback(fireEvent);

  /**
   * Trash draw: a cross.
   *
   * @private
   * @type {object}
   */
  var trash = new Konva.Group();

  // first line of the cross
  var trashLine1 = new Konva.Line({
    points: [-10, -10, 10, 10],
    stroke: 'red'
  });
    // second line of the cross
  var trashLine2 = new Konva.Line({
    points: [10, -10, -10, 10],
    stroke: 'red'
  });
  trash.width(20);
  trash.height(20);
  trash.add(trashLine1);
  trash.add(trashLine2);

  /**
   * Drawing style.
   *
   * @type {dwv.gui.Style}
   */
  this.style = app.getStyle();

  /**
   * Event listeners.
   *
   * @private
   */
  var listeners = {};

  /**
   * Handle mouse down event.
   *
   * @param {object} event The mouse down event.
   */
  this.mousedown = function (event) {
    // exit if a draw was started (handle at mouse move or up)
    if (started) {
      return;
    }

    var layerDetails = dwv.gui.getLayerDetailsFromEvent(event);
    var layerGroup = app.getLayerGroupById(layerDetails.groupId);
    var drawLayer = layerGroup.getActiveDrawLayer();

    // determine if the click happened in an existing shape
    var stage = drawLayer.getKonvaStage();
    var kshape = stage.getIntersection({
      x: event._x,
      y: event._y
    });

    // update scale
    self.style.setZoomScale(stage.scale());

    if (kshape) {
      var group = kshape.getParent();
      var selectedShape = group.find('.shape')[0];
      // reset editor if click on other shape
      // (and avoid anchors mouse down)
      if (selectedShape && selectedShape !== shapeEditor.getShape()) {
        shapeEditor.disable();
        shapeEditor.setShape(selectedShape);
        var viewController =
          layerGroup.getActiveViewLayer().getViewController();
        shapeEditor.setViewController(viewController);
        shapeEditor.enable();
      }
    } else {
      // disable edition
      shapeEditor.disable();
      shapeEditor.setShape(null);
      shapeEditor.setViewController(null);
      // start storing points
      started = true;
      // set factory
      currentFactory = new self.shapeFactoryList[self.shapeName]();
      // clear array
      points = [];
      // store point
      var viewLayer = layerGroup.getActiveViewLayer();
      var pos = viewLayer.displayToPlanePos(event._x, event._y);
      lastPoint = new dwv.math.Point2D(pos.x, pos.y);
      points.push(lastPoint);
    }
  };

  /**
   * Handle mouse move event.
   *
   * @param {object} event The mouse move event.
   */
  this.mousemove = function (event) {
    // exit if not started draw
    if (!started) {
      return;
    }

    var layerDetails = dwv.gui.getLayerDetailsFromEvent(event);
    var layerGroup = app.getLayerGroupById(layerDetails.groupId);
    var viewLayer = layerGroup.getActiveViewLayer();
    var pos = viewLayer.displayToPlanePos(event._x, event._y);

    // draw line to current pos
    if (Math.abs(pos.x - lastPoint.getX()) > 0 ||
      Math.abs(pos.y - lastPoint.getY()) > 0) {
      // clear last added point from the list (but not the first one)
      // if it was marked as temporary
      if (points.length !== 1 &&
        typeof points[points.length - 1].tmp !== 'undefined') {
        points.pop();
      }
      // current point
      lastPoint = new dwv.math.Point2D(pos.x, pos.y);
      // mark it as temporary
      lastPoint.tmp = true;
      // add it to the list
      points.push(lastPoint);
      // update points
      onNewPoints(points, layerGroup);
    }
  };

  /**
   * Handle mouse up event.
   *
   * @param {object} event The mouse up event.
   */
  this.mouseup = function (event) {
    // exit if not started draw
    if (!started) {
      return;
    }
    // exit if no points
    if (points.length === 0) {
      dwv.logger.warn('Draw mouseup but no points...');
      return;
    }

    // do we have all the needed points
    if (points.length === currentFactory.getNPoints()) {
      // store points
      var layerDetails = dwv.gui.getLayerDetailsFromEvent(event);
      var layerGroup = app.getLayerGroupById(layerDetails.groupId);
      onFinalPoints(points, layerGroup);
      // reset flag
      started = false;
    } else {
      // remove temporary flag
      if (typeof points[points.length - 1].tmp !== 'undefined') {
        delete points[points.length - 1].tmp;
      }
    }
  };

  /**
   * Handle double click event.
   *
   * @param {object} event The mouse up event.
   */
  this.dblclick = function (event) {
    // exit if not started draw
    if (!started) {
      return;
    }
    // exit if no points
    if (points.length === 0) {
      dwv.logger.warn('Draw dblclick but no points...');
      return;
    }

    // store points
    var layerDetails = dwv.gui.getLayerDetailsFromEvent(event);
    var layerGroup = app.getLayerGroupById(layerDetails.groupId);
    onFinalPoints(points, layerGroup);
    // reset flag
    started = false;
  };

  /**
   * Handle mouse out event.
   *
   * @param {object} event The mouse out event.
   */
  this.mouseout = function (event) {
    self.mouseup(event);
  };

  /**
   * Handle touch start event.
   *
   * @param {object} event The touch start event.
   */
  this.touchstart = function (event) {
    self.mousedown(event);
  };

  /**
   * Handle touch move event.
   *
   * @param {object} event The touch move event.
   */
  this.touchmove = function (event) {
    // exit if not started draw
    if (!started) {
      return;
    }

    var layerDetails = dwv.gui.getLayerDetailsFromEvent(event);
    var layerGroup = app.getLayerGroupById(layerDetails.groupId);
    var viewLayer = layerGroup.getActiveViewLayer();
    var pos = viewLayer.displayToPlanePos(event._x, event._y);

    if (Math.abs(pos.x - lastPoint.getX()) > 0 ||
      Math.abs(pos.y - lastPoint.getY()) > 0) {
      // clear last added point from the list (but not the first one)
      if (points.length !== 1) {
        points.pop();
      }
      // current point
      lastPoint = new dwv.math.Point2D(pos.x, pos.y);
      // add current one to the list
      points.push(lastPoint);
      // allow for anchor points
      if (points.length < currentFactory.getNPoints()) {
        clearTimeout(this.timer);
        this.timer = setTimeout(function () {
          points.push(lastPoint);
        }, currentFactory.getTimeout());
      }
      // update points
      onNewPoints(points, layerGroup);
    }
  };

  /**
   * Handle touch end event.
   *
   * @param {object} event The touch end event.
   */
  this.touchend = function (event) {
    self.dblclick(event);
  };

  /**
   * Handle key down event.
   *
   * @param {object} event The key down event.
   */
  this.keydown = function (event) {
    // call app handler if we are not in the middle of a draw
    if (!started) {
      event.context = 'dwv.tool.Draw';
      app.onKeydown(event);
    }
    var konvaLayer;

    // press delete key
    if (event.keyCode === 46 && shapeEditor.isActive()) {
      // get shape
      var shapeGroup = shapeEditor.getShape().getParent();
      konvaLayer = shapeGroup.getLayer();
      var shapeDisplayName = dwv.tool.GetShapeDisplayName(
        shapeGroup.getChildren(dwv.draw.isNodeNameShape)[0]);
      // delete command
      var delcmd = new dwv.tool.DeleteGroupCommand(shapeGroup,
        shapeDisplayName, konvaLayer);
      delcmd.onExecute = fireEvent;
      delcmd.onUndo = fireEvent;
      delcmd.execute();
      app.addToUndoStack(delcmd);
    }

    // escape key: exit shape creation
    if (event.keyCode === 27 && tmpShapeGroup !== null) {
      konvaLayer = tmpShapeGroup.getLayer();
      // reset temporary shape group
      tmpShapeGroup.destroy();
      tmpShapeGroup = null;
      // reset flag and points
      started = false;
      points = [];
      // redraw
      konvaLayer.draw();
    }
  };

  /**
   * Update the current draw with new points.
   *
   * @param {Array} tmpPoints The array of new points.
   * @param {dwv.gui.LayerGroup} layerGroup The origin layer group.
   */
  function onNewPoints(tmpPoints, layerGroup) {
    var drawLayer = layerGroup.getActiveDrawLayer();
    var konvaLayer = drawLayer.getKonvaLayer();

    // remove temporary shape draw
    if (tmpShapeGroup) {
      tmpShapeGroup.destroy();
      tmpShapeGroup = null;
    }

    // create shape group
    var viewController =
      layerGroup.getActiveViewLayer().getViewController();
    tmpShapeGroup = currentFactory.create(
      tmpPoints, self.style, viewController);
    // do not listen during creation
    var shape = tmpShapeGroup.getChildren(dwv.draw.isNodeNameShape)[0];
    shape.listening(false);
    konvaLayer.listening(false);
    // draw shape
    konvaLayer.add(tmpShapeGroup);
    konvaLayer.draw();
  }

  /**
   * Create the final shape from a point list.
   *
   * @param {Array} finalPoints The array of points.
   * @param {dwv.gui.LayerGroup} layerGroup The origin layer group.
   */
  function onFinalPoints(finalPoints, layerGroup) {
    var drawLayer = layerGroup.getActiveDrawLayer();
    var konvaLayer = drawLayer.getKonvaLayer();

    // reset temporary shape group
    if (tmpShapeGroup) {
      tmpShapeGroup.destroy();
      tmpShapeGroup = null;
    }

    var viewController =
      layerGroup.getActiveViewLayer().getViewController();
    var drawController =
      layerGroup.getActiveDrawLayer().getDrawController();

    // create final shape
    var finalShapeGroup = currentFactory.create(
      finalPoints, self.style, viewController);
    finalShapeGroup.id(dwv.math.guid());

    // get the position group
    var posGroup = drawController.getCurrentPosGroup();
    // add shape group to position group
    posGroup.add(finalShapeGroup);

    // re-activate layer
    konvaLayer.listening(true);
    // draw shape command
    command = new dwv.tool.DrawGroupCommand(
      finalShapeGroup, self.shapeName, konvaLayer);
    command.onExecute = fireEvent;
    command.onUndo = fireEvent;
    // execute it
    command.execute();
    // save it in undo stack
    app.addToUndoStack(command);

    // activate shape listeners
    self.setShapeOn(finalShapeGroup, layerGroup);
  }

  /**
   * Activate the tool.
   *
   * @param {boolean} flag The flag to activate or not.
   */
  this.activate = function (flag) {
    // reset shape display properties
    shapeEditor.disable();
    shapeEditor.setShape(null);
    shapeEditor.setViewController(null);
    document.body.style.cursor = 'default';
    // get the current draw layer
    var layerGroup = app.getActiveLayerGroup();
    activateCurrentPositionShapes(flag, layerGroup);
    // listen to app change to update the draw layer
    if (flag) {
      // TODO: merge with drawController.activateDrawLayer?
      app.addEventListener('positionchange', function () {
        updateDrawLayer(layerGroup);
      });
      // same for colour
      this.setLineColour(this.style.getLineColour());
    } else {
      app.removeEventListener('positionchange', function () {
        updateDrawLayer(layerGroup);
      });
    }
  };

  /**
   * Update the draw layer.
   *
   * @param {dwv.gui.LayerGroup} layerGroup The origin layer group.
   */
  function updateDrawLayer(layerGroup) {
    // activate the shape at current position
    activateCurrentPositionShapes(true, layerGroup);
  }

  /**
   * Activate shapes at current position.
   *
   * @param {boolean} visible Set the draw layer visible or not.
   * @param {dwv.gui.LayerGroup} layerGroup The origin layer group.
   */
  function activateCurrentPositionShapes(visible, layerGroup) {
    var drawController =
      layerGroup.getActiveDrawLayer().getDrawController();

    // get shape groups at the current position
    var shapeGroups =
      drawController.getCurrentPosGroup().getChildren();

    // set shape display properties
    if (visible) {
      // activate shape listeners
      shapeGroups.forEach(function (group) {
        self.setShapeOn(group, layerGroup);
      });
    } else {
      // de-activate shape listeners
      shapeGroups.forEach(function (group) {
        setShapeOff(group);
      });
    }
    // draw
    var drawLayer = layerGroup.getActiveDrawLayer();
    var konvaLayer = drawLayer.getKonvaLayer();
    konvaLayer.draw();
  }

  /**
   * Set shape group off properties.
   *
   * @param {object} shapeGroup The shape group to set off.
   */
  function setShapeOff(shapeGroup) {
    // mouse styling
    shapeGroup.off('mouseover');
    shapeGroup.off('mouseout');
    // drag
    shapeGroup.draggable(false);
    shapeGroup.off('dragstart.draw');
    shapeGroup.off('dragmove.draw');
    shapeGroup.off('dragend.draw');
    shapeGroup.off('dblclick');
  }

  /**
   * Get the real position from an event.
   * TODO: use layer method?
   *
   * @param {object} index The input index as {x,y}.
   * @param {dwv.gui.LayerGroup} layerGroup The origin layer group.
   * @returns {object} The real position in the image as {x,y}.
   * @private
   */
  function getRealPosition(index, layerGroup) {
    var drawLayer = layerGroup.getActiveDrawLayer();
    var stage = drawLayer.getKonvaStage();
    return {
      x: stage.offset().x + index.x / stage.scale().x,
      y: stage.offset().y + index.y / stage.scale().y
    };
  }

  /**
   * Set shape group on properties.
   *
   * @param {object} shapeGroup The shape group to set on.
   * @param {dwv.gui.LayerGroup} layerGroup The origin layer group.
   */
  this.setShapeOn = function (shapeGroup, layerGroup) {
    // mouse over styling
    shapeGroup.on('mouseover', function () {
      document.body.style.cursor = 'pointer';
    });
    // mouse out styling
    shapeGroup.on('mouseout', function () {
      document.body.style.cursor = 'default';
    });

    var drawLayer = layerGroup.getActiveDrawLayer();
    var konvaLayer = drawLayer.getKonvaLayer();

    // make it draggable
    shapeGroup.draggable(true);
    // cache drag start position
    var dragStartPos = {x: shapeGroup.x(), y: shapeGroup.y()};

    // command name based on shape type
    var shapeDisplayName = dwv.tool.GetShapeDisplayName(
      shapeGroup.getChildren(dwv.draw.isNodeNameShape)[0]);

    var colour = null;

    // drag start event handling
    shapeGroup.on('dragstart.draw', function (/*event*/) {
      // store colour
      colour = shapeGroup.getChildren(dwv.draw.isNodeNameShape)[0].stroke();
      // display trash
      var drawLayer = layerGroup.getActiveDrawLayer();
      var stage = drawLayer.getKonvaStage();
      var scale = stage.scale();
      var invscale = {x: 1 / scale.x, y: 1 / scale.y};
      trash.x(stage.offset().x + (stage.width() / (2 * scale.x)));
      trash.y(stage.offset().y + (stage.height() / (15 * scale.y)));
      trash.scale(invscale);
      konvaLayer.add(trash);
      // deactivate anchors to avoid events on null shape
      shapeEditor.setAnchorsActive(false);
      // draw
      konvaLayer.draw();
    });
    // drag move event handling
    shapeGroup.on('dragmove.draw', function (event) {
      var drawLayer = layerGroup.getActiveDrawLayer();
      // validate the group position
      dwv.tool.validateGroupPosition(drawLayer.getBaseSize(), this);
      // update quantification if possible
      if (typeof currentFactory.updateQuantification !== 'undefined') {
        var vc = layerGroup.getActiveViewLayer().getViewController();
        currentFactory.updateQuantification(this, vc);
      }
      // highlight trash when on it
      var offset = dwv.gui.getEventOffset(event.evt)[0];
      var eventPos = getRealPosition(offset, layerGroup);
      var trashHalfWidth = trash.width() * trash.scaleX() / 2;
      var trashHalfHeight = trash.height() * trash.scaleY() / 2;
      if (Math.abs(eventPos.x - trash.x()) < trashHalfWidth &&
        Math.abs(eventPos.y - trash.y()) < trashHalfHeight) {
        trash.getChildren().forEach(function (tshape) {
          tshape.stroke('orange');
        });
        // change the group shapes colour
        shapeGroup.getChildren(dwv.draw.canNodeChangeColour).forEach(
          function (ashape) {
            ashape.stroke('red');
          });
      } else {
        trash.getChildren().forEach(function (tshape) {
          tshape.stroke('red');
        });
        // reset the group shapes colour
        shapeGroup.getChildren(dwv.draw.canNodeChangeColour).forEach(
          function (ashape) {
            if (typeof ashape.stroke !== 'undefined') {
              ashape.stroke(colour);
            }
          });
      }
      // draw
      konvaLayer.draw();
    });
    // drag end event handling
    shapeGroup.on('dragend.draw', function (event) {
      var pos = {x: this.x(), y: this.y()};
      // remove trash
      trash.remove();
      // delete case
      var offset = dwv.gui.getEventOffset(event.evt)[0];
      var eventPos = getRealPosition(offset, layerGroup);
      var trashHalfWidth = trash.width() * trash.scaleX() / 2;
      var trashHalfHeight = trash.height() * trash.scaleY() / 2;
      if (Math.abs(eventPos.x - trash.x()) < trashHalfWidth &&
        Math.abs(eventPos.y - trash.y()) < trashHalfHeight) {
        // compensate for the drag translation
        this.x(dragStartPos.x);
        this.y(dragStartPos.y);
        // disable editor
        shapeEditor.disable();
        shapeEditor.setShape(null);
        shapeEditor.setViewController(null);
        // reset colour
        shapeGroup.getChildren(dwv.draw.canNodeChangeColour).forEach(
          function (ashape) {
            ashape.stroke(colour);
          });
        // reset cursor
        document.body.style.cursor = 'default';
        // delete command
        var delcmd = new dwv.tool.DeleteGroupCommand(this,
          shapeDisplayName, konvaLayer);
        delcmd.onExecute = fireEvent;
        delcmd.onUndo = fireEvent;
        delcmd.execute();
        app.addToUndoStack(delcmd);
      } else {
        // save drag move
        var translation = {x: pos.x - dragStartPos.x,
          y: pos.y - dragStartPos.y};
        if (translation.x !== 0 || translation.y !== 0) {
          var mvcmd = new dwv.tool.MoveGroupCommand(this,
            shapeDisplayName, translation, konvaLayer);
          mvcmd.onExecute = fireEvent;
          mvcmd.onUndo = fireEvent;
          app.addToUndoStack(mvcmd);

          // the move is handled by Konva, trigger an event manually
          fireEvent({
            type: 'drawmove',
            id: this.id()
          });
        }
        // reset anchors
        shapeEditor.setAnchorsActive(true);
        shapeEditor.resetAnchors();
      }
      // draw
      konvaLayer.draw();
      // reset start position
      dragStartPos = {x: this.x(), y: this.y()};
    });
    // double click handling: update label
    shapeGroup.on('dblclick', function () {
      // get the label object for this shape
      var label = this.findOne('Label');
      // should just be one
      if (typeof label === 'undefined') {
        throw new Error('Could not find the shape label.');
      }
      var ktext = label.getText();

      var onSaveCallback = function (meta) {
        // store meta
        ktext.meta = meta;
        // update text expression
        ktext.setText(dwv.utils.replaceFlags(
          ktext.meta.textExpr, ktext.meta.quantification));
        label.setVisible(ktext.meta.textExpr.length !== 0);

        // trigger event
        fireEvent({
          type: 'drawchange'
        });
        // draw
        konvaLayer.draw();
      };

      // call client dialog if defined
      if (typeof dwv.openRoiDialog !== 'undefined') {
        dwv.openRoiDialog(ktext.meta, onSaveCallback);
      } else {
        // simple prompt for the text expression
        var textExpr = dwv.prompt('Label', ktext.meta.textExpr);
        if (textExpr !== null) {
          ktext.meta.textExpr = textExpr;
          onSaveCallback(ktext.meta);
        }
      }
    });
  };

  /**
   * Set the tool options.
   *
   * @param {object} options The list of shape names amd classes.
   */
  this.setOptions = function (options) {
    // save the options as the shape factory list
    this.shapeFactoryList = options;
    // pass them to the editor
    shapeEditor.setFactoryList(options);
  };

  /**
   * Initialise the tool.
   */
  this.init = function () {
    // does nothing
  };

  /**
   * Add an event listener on the app.
   *
   * @param {string} type The event type.
   * @param {object} listener The method associated with the provided
   *   event type.
   */
  this.addEventListener = function (type, listener) {
    if (typeof listeners[type] === 'undefined') {
      listeners[type] = [];
    }
    listeners[type].push(listener);
  };

  /**
   * Remove an event listener from the app.
   *
   * @param {string} type The event type.
   * @param {object} listener The method associated with the provided
   *   event type.
   */
  this.removeEventListener = function (type, listener) {
    if (typeof listeners[type] === 'undefined') {
      return;
    }
    for (var i = 0; i < listeners[type].length; ++i) {
      if (listeners[type][i] === listener) {
        listeners[type].splice(i, 1);
      }
    }
  };

  /**
   * Set the line colour of the drawing.
   *
   * @param {string} colour The colour to set
   */
  this.setLineColour = function (colour) {
    this.style.setLineColour(colour);
  };

  // Private Methods -----------------------------------------------------------

  /**
   * Fire an event: call all associated listeners.
   *
   * @param {object} event The event to fire.
   * @private
   */
  function fireEvent(event) {
    if (typeof listeners[event.type] === 'undefined') {
      return;
    }
    for (var i = 0; i < listeners[event.type].length; ++i) {
      listeners[event.type][i](event);
    }
  }

}; // Draw class

/**
 * Help for this tool.
 *
 * @returns {object} The help content.
 */
dwv.tool.Draw.prototype.getHelpKeys = function () {
  return {
    title: 'tool.Draw.name',
    brief: 'tool.Draw.brief',
    mouse: {
      mouse_drag: 'tool.Draw.mouse_drag'
    },
    touch: {
      touch_drag: 'tool.Draw.touch_drag'
    }
  };
};

/**
 * Set the shape name of the drawing.
 *
 * @param {string} name The name of the shape.
 */
dwv.tool.Draw.prototype.setShapeName = function (name) {
  // check if we have it
  if (!this.hasShape(name)) {
    throw new Error('Unknown shape: \'' + name + '\'');
  }
  this.shapeName = name;
};

/**
 * Check if the shape is in the shape list.
 *
 * @param {string} name The name of the shape.
 * @returns {boolean} True if there is a factory for the shape.
 */
dwv.tool.Draw.prototype.hasShape = function (name) {
  return this.shapeFactoryList[name];
};

/**
 * Get the minimum position in a groups' anchors.
 *
 * @param {object} group The group that contains anchors.
 * @returns {object} The minimum position as {x,y}.
 */
dwv.tool.getAnchorMin = function (group) {
  var anchors = group.find('.anchor');
  if (anchors.length === 0) {
    return;
  }
  var minX = anchors[0].x();
  var minY = anchors[0].y();
  for (var i = 0; i < anchors.length; ++i) {
    minX = Math.min(minX, anchors[i].x());
    minY = Math.min(minY, anchors[i].y());
  }

  return {x: minX, y: minY};
};

/**
 * Bound a node position.
 *
 * @param {object} node The node to bound the position.
 * @param {object} min The minimum position as {x,y}.
 * @param {object} max The maximum position as {x,y}.
 * @returns {boolean} True if the position was corrected.
 */
dwv.tool.boundNodePosition = function (node, min, max) {
  var changed = false;
  if (node.x() < min.x) {
    node.x(min.x);
    changed = true;
  } else if (node.x() > max.x) {
    node.x(max.x);
    changed = true;
  }
  if (node.y() < min.y) {
    node.y(min.y);
    changed = true;
  } else if (node.y() > max.y) {
    node.y(max.y);
    changed = true;
  }
  return changed;
};

/**
 * Validate a group position.
 *
 * @param {object} stageSize The stage size {x,y}.
 * @param {object} group The group to evaluate.
 * @returns {boolean} True if the position was corrected.
 */
dwv.tool.validateGroupPosition = function (stageSize, group) {
  // if anchors get mixed, width/height can be negative
  var shape = group.getChildren(dwv.draw.isNodeNameShape)[0];
  var anchorMin = dwv.tool.getAnchorMin(group);
  // handle no anchor: when dragging the label, the editor does
  //   not activate
  if (typeof anchorMin === 'undefined') {
    return null;
  }

  var min = {
    x: -anchorMin.x,
    y: -anchorMin.y
  };
  var max = {
    x: stageSize.x -
      (anchorMin.x + Math.abs(shape.width())),
    y: stageSize.y -
      (anchorMin.y + Math.abs(shape.height()))
  };

  return dwv.tool.boundNodePosition(group, min, max);
};

/**
 * Validate an anchor position.
 *
 * @param {object} stageSize The stage size {x,y}.
 * @param {object} anchor The anchor to evaluate.
 * @returns {boolean} True if the position was corrected.
 */
dwv.tool.validateAnchorPosition = function (stageSize, anchor) {
  var group = anchor.getParent();

  var min = {
    x: -group.x(),
    y: -group.y()
  };
  var max = {
    x: stageSize.x - group.x(),
    y: stageSize.y - group.y()
  };

  return dwv.tool.boundNodePosition(anchor, min, max);
};