src/tools/floodfill.js

// namespaces
var dwv = dwv || {};
dwv.tool = dwv.tool || {};
/**
 * The magic wand namespace.
 *
 * @external MagicWand
 * @see https://github.com/Tamersoul/magic-wand-js
 */
var MagicWand = MagicWand || {};

/**
 * Floodfill painting tool.
 *
 * @class
 * @param {dwv.App} app The associated application.
 */
dwv.tool.Floodfill = function (app) {
  /**
   * Original variables from external library. Used as in the lib example.
   *
   * @private
   * @type {number}
   */
  var blurRadius = 5;
  /**
   * Original variables from external library. Used as in the lib example.
   *
   * @private
   * @type {number}
   */
  var simplifyTolerant = 0;
  /**
   * Original variables from external library. Used as in the lib example.
   *
   * @private
   * @type {number}
   */
  var simplifyCount = 2000;
  /**
   * Canvas info
   *
   * @private
   * @type {object}
   */
  var imageInfo = null;
  /**
   * Object created by MagicWand lib containing border points
   *
   * @private
   * @type {object}
   */
  var mask = null;
  /**
   * threshold default tolerance of the tool border
   *
   * @private
   * @type {number}
   */
  var initialthreshold = 10;
  /**
   * threshold tolerance of the tool border
   *
   * @private
   * @type {number}
   */
  var currentthreshold = null;
  /**
   * Closure to self: to be used by event handlers.
   *
   * @private
   * @type {dwv.tool.Floodfill}
   */
  var self = this;
  /**
   * Interaction start flag.
   *
   * @type {boolean}
   */
  this.started = false;
  /**
   * Draw command.
   *
   * @private
   * @type {object}
   */
  var command = null;
  /**
   * Current shape group.
   *
   * @private
   * @type {object}
   */
  var shapeGroup = null;
  /**
   * Coordinates of the fist mousedown event.
   *
   * @private
   * @type {object}
   */
  var initialpoint;
  /**
   * Floodfill border.
   *
   * @private
   * @type {object}
   */
  var border = null;
  /**
   * List of parent points.
   *
   * @private
   * @type {Array}
   */
  var parentPoints = [];
  /**
   * Assistant variable to paint border on all slices.
   *
   * @private
   * @type {boolean}
   */
  var extender = false;
  /**
   * Timeout for painting on mousemove.
   *
   * @private
   */
  var painterTimeout;
  /**
   * Drawing style.
   *
   * @type {dwv.gui.Style}
   */
  this.style = new dwv.gui.Style();

  /**
   * Listener handler.
   *
   * @type {object}
   * @private
   */
  var listenerHandler = new dwv.utils.ListenerHandler();

  /**
   * Set extend option for painting border on all slices.
   *
   * @param {boolean} bool The option to set
   */
  this.setExtend = function (bool) {
    extender = bool;
  };

  /**
   * Get extend option for painting border on all slices.
   *
   * @returns {boolean} The actual value of of the variable to use Floodfill
   *   on museup.
   */
  this.getExtend = function () {
    return extender;
  };

  /**
   * Get (x, y) coordinates referenced to the canvas
   *
   * @param {object} event The original event.
   * @returns {object} The coordinates as a {x,y}.
   * @private
   */
  var getCoord = function (event) {
    var layerDetails = dwv.gui.getLayerDetailsFromEvent(event);
    var layerGroup = app.getLayerGroupById(layerDetails.groupId);
    var viewLayer = layerGroup.getActiveViewLayer();
    var index = viewLayer.displayToPlaneIndex(event._x, event._y);
    return {
      x: index.get(0),
      y: index.get(1)
    };
  };

  /**
   * Calculate border.
   *
   * @private
   * @param {object} points The input points.
   * @param {number} threshold The threshold of the floodfill.
   * @param {boolean} simple Return first points or a list.
   * @returns {Array} The parent points.
   */
  var calcBorder = function (points, threshold, simple) {

    parentPoints = [];
    var image = {
      data: imageInfo.data,
      width: imageInfo.width,
      height: imageInfo.height,
      bytes: 4
    };

    mask = MagicWand.floodFill(image, points.x, points.y, threshold);
    mask = MagicWand.gaussBlurOnlyBorder(mask, blurRadius);

    var cs = MagicWand.traceContours(mask);
    cs = MagicWand.simplifyContours(cs, simplifyTolerant, simplifyCount);

    if (cs.length > 0 && cs[0].points[0].x) {
      if (simple) {
        return cs[0].points;
      }
      for (var j = 0, icsl = cs[0].points.length; j < icsl; j++) {
        parentPoints.push(new dwv.math.Point2D(
          cs[0].points[j].x,
          cs[0].points[j].y
        ));
      }
      return parentPoints;
    } else {
      return false;
    }
  };

  /**
   * Paint Floodfill.
   *
   * @private
   * @param {object} point The start point.
   * @param {number} threshold The border threshold.
   * @param {object} layerGroup The origin layer group.
   * @returns {boolean} False if no border.
   */
  var paintBorder = function (point, threshold, layerGroup) {
    // Calculate the border
    border = calcBorder(point, threshold);
    // Paint the border
    if (border) {
      var factory = new dwv.tool.draw.RoiFactory();
      shapeGroup = factory.create(border, self.style);
      shapeGroup.id(dwv.math.guid());

      var drawLayer = layerGroup.getActiveDrawLayer();
      var drawController = drawLayer.getDrawController();

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

      // draw shape command
      command = new dwv.tool.DrawGroupCommand(shapeGroup, 'floodfill',
        drawLayer.getKonvaLayer());
      command.onExecute = fireEvent;
      command.onUndo = fireEvent;
      // // draw
      command.execute();
      // save it in undo stack
      app.addToUndoStack(command);

      return true;
    } else {
      return false;
    }
  };

  /**
   * Create Floodfill in all the prev and next slices while border is found
   *
   * @param {number} ini The first slice to extend to.
   * @param {number} end The last slice to extend to.
   * @param {object} layerGroup The origin layer group.
   */
  this.extend = function (ini, end, layerGroup) {
    //avoid errors
    if (!initialpoint) {
      throw '\'initialpoint\' not found. User must click before use extend!';
    }
    // remove previous draw
    if (shapeGroup) {
      shapeGroup.destroy();
    }

    var viewController =
      layerGroup.getActiveViewLayer().getViewController();

    var pos = viewController.getCurrentIndex();
    var imageSize = viewController.getImageSize();
    var threshold = currentthreshold || initialthreshold;

    // Iterate over the next images and paint border on each slice.
    for (var i = pos.get(2),
      len = end
        ? end : imageSize.get(2);
      i < len; i++) {
      if (!paintBorder(initialpoint, threshold, layerGroup)) {
        break;
      }
      viewController.incrementIndex(2);
    }
    viewController.setCurrentPosition(pos);

    // Iterate over the prev images and paint border on each slice.
    for (var j = pos.get(2), jl = ini ? ini : 0; j > jl; j--) {
      if (!paintBorder(initialpoint, threshold, layerGroup)) {
        break;
      }
      viewController.decrementIndex(2);
    }
    viewController.setCurrentPosition(pos);
  };

  /**
   * Modify tolerance threshold and redraw ROI.
   *
   * @param {number} modifyThreshold The new threshold.
   * @param {shape} shape The shape to update.
   */
  this.modifyThreshold = function (modifyThreshold, shape) {

    if (!shape && shapeGroup) {
      shape = shapeGroup.getChildren(function (node) {
        return node.name() === 'shape';
      })[0];
    } else {
      throw 'No shape found';
    }

    clearTimeout(painterTimeout);
    painterTimeout = setTimeout(function () {
      border = calcBorder(initialpoint, modifyThreshold, true);
      if (!border) {
        return false;
      }
      var arr = [];
      for (var i = 0, bl = border.length; i < bl; ++i) {
        arr.push(border[i].x);
        arr.push(border[i].y);
      }
      shape.setPoints(arr);
      var shapeLayer = shape.getLayer();
      shapeLayer.draw();
      self.onThresholdChange(modifyThreshold);
    }, 100);
  };

  /**
   * Event fired when threshold change
   *
   * @param {number} _value Current threshold
   */
  this.onThresholdChange = function (_value) {
    // Defaults do nothing
  };

  /**
   * Handle mouse down event.
   *
   * @param {object} event The mouse down event.
   */
  this.mousedown = function (event) {
    var layerDetails = dwv.gui.getLayerDetailsFromEvent(event);
    var layerGroup = app.getLayerGroupById(layerDetails.groupId);
    var viewLayer = layerGroup.getActiveViewLayer();
    var drawLayer = layerGroup.getActiveDrawLayer();

    imageInfo = viewLayer.getImageData();
    if (!imageInfo) {
      dwv.logger.error('No image found');
      return;
    }

    // update zoom scale
    self.style.setZoomScale(
      drawLayer.getKonvaLayer().getAbsoluteScale());

    self.started = true;
    initialpoint = getCoord(event);
    paintBorder(initialpoint, initialthreshold, layerGroup);
    self.onThresholdChange(initialthreshold);
  };

  /**
   * Handle mouse move event.
   *
   * @param {object} event The mouse move event.
   */
  this.mousemove = function (event) {
    if (!self.started) {
      return;
    }
    var movedpoint = getCoord(event);
    currentthreshold = Math.round(Math.sqrt(
      Math.pow((initialpoint.x - movedpoint.x), 2) +
      Math.pow((initialpoint.y - movedpoint.y), 2)) / 2);
    currentthreshold = currentthreshold < initialthreshold
      ? initialthreshold : currentthreshold - initialthreshold;
    self.modifyThreshold(currentthreshold);
  };

  /**
   * Handle mouse up event.
   *
   * @param {object} _event The mouse up event.
   */
  this.mouseup = function (_event) {
    self.started = false;
    if (extender) {
      var layerDetails = dwv.gui.getLayerDetailsFromEvent(event);
      var layerGroup = app.getLayerGroupById(layerDetails.groupId);
      self.extend(layerGroup);
    }
  };

  /**
   * 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) {
    // treat as mouse down
    self.mousedown(event);
  };

  /**
   * Handle touch move event.
   *
   * @param {object} event The touch move event.
   */
  this.touchmove = function (event) {
    // treat as mouse move
    self.mousemove(event);
  };

  /**
   * Handle touch end event.
   *
   * @param {object} event The touch end event.
   */
  this.touchend = function (event) {
    // treat as mouse up
    self.mouseup(event);
  };

  /**
   * Handle key down event.
   *
   * @param {object} event The key down event.
   */
  this.keydown = function (event) {
    event.context = 'dwv.tool.Floodfill';
    app.onKeydown(event);
  };

  /**
   * Activate the tool.
   *
   * @param {boolean} bool The flag to activate or not.
   */
  this.activate = function (bool) {
    if (bool) {
      // init with the app window scale
      this.style.setBaseScale(app.getBaseScale());
      // set the default to the first in the list
      this.setLineColour(this.style.getLineColour());
    }
  };

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

  /**
   * Add an event listener to this class.
   *
   * @param {string} type The event type.
   * @param {object} callback The method associated with the provided
   *   event type, will be called with the fired event.
   */
  this.addEventListener = function (type, callback) {
    listenerHandler.add(type, callback);
  };
  /**
   * Remove an event listener from this class.
   *
   * @param {string} type The event type.
   * @param {object} callback The method associated with the provided
   *   event type.
   */
  this.removeEventListener = function (type, callback) {
    listenerHandler.remove(type, callback);
  };
  /**
   * Fire an event: call all associated listeners with the input event object.
   *
   * @param {object} event The event to fire.
   * @private
   */
  function fireEvent(event) {
    listenerHandler.fireEvent(event);
  }

}; // Floodfill class

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

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