BOA_.java

package com.github.celldynamics.quimp;

import java.awt.BorderLayout;
import java.awt.Button;
import java.awt.Checkbox;
import java.awt.CheckboxMenuItem;
import java.awt.Choice;
import java.awt.Color;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.Graphics;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.GridLayout;
import java.awt.Label;
import java.awt.Menu;
import java.awt.MenuBar;
import java.awt.MenuItem;
import java.awt.Panel;
import java.awt.Rectangle;
import java.awt.TextArea;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.ItemEvent;
import java.awt.event.ItemListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.URI;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ExecutionException;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

import javax.swing.BoxLayout;
import javax.swing.JOptionPane;
import javax.swing.JScrollPane;
import javax.swing.JSpinner;
import javax.swing.SpinnerNumberModel;
import javax.swing.SwingWorker;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;

import org.scijava.vecmath.Point2d;
import org.scijava.vecmath.Vector2d;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.github.celldynamics.quimp.BOAState.BOAp;
import com.github.celldynamics.quimp.BOAState.SegParam;
import com.github.celldynamics.quimp.QuimpException.MessageSinkTypes;
import com.github.celldynamics.quimp.SnakePluginList.Plugin;
import com.github.celldynamics.quimp.filesystem.DataContainer;
import com.github.celldynamics.quimp.filesystem.DataContainerInstanceCreator;
import com.github.celldynamics.quimp.filesystem.FileDialogEx;
import com.github.celldynamics.quimp.filesystem.FileExtensions;
import com.github.celldynamics.quimp.filesystem.StatsCollection;
import com.github.celldynamics.quimp.filesystem.versions.Converter170202;
import com.github.celldynamics.quimp.plugin.IQuimpCorePlugin;
import com.github.celldynamics.quimp.plugin.IQuimpPluginAttachImage;
import com.github.celldynamics.quimp.plugin.QuimpPluginException;
import com.github.celldynamics.quimp.plugin.binaryseg.BinarySegmentation_;
import com.github.celldynamics.quimp.plugin.engine.PluginFactory;
import com.github.celldynamics.quimp.plugin.engine.PluginProperties;
import com.github.celldynamics.quimp.plugin.snakes.IQuimpBOAPoint2dFilter;
import com.github.celldynamics.quimp.plugin.snakes.IQuimpBOASnakeFilter;
import com.github.celldynamics.quimp.plugin.utils.QuimpDataConverter;
import com.github.celldynamics.quimp.registration.Registration;
import com.github.celldynamics.quimp.utils.QuimpToolsCollection;
import com.github.celldynamics.quimp.utils.graphics.GraphicsElements;
import com.google.gson.JsonSyntaxException;

import ij.IJ;
import ij.ImagePlus;
import ij.ImageStack;
import ij.WindowManager;
import ij.gui.GenericDialog;
import ij.gui.ImageCanvas;
import ij.gui.NewImage;
import ij.gui.Overlay;
import ij.gui.PointRoi;
import ij.gui.PolygonRoi;
import ij.gui.Roi;
import ij.gui.StackWindow;
import ij.gui.TextRoi;
import ij.gui.Toolbar;
import ij.gui.YesNoCancelDialog;
import ij.io.OpenDialog;
import ij.io.SaveDialog;
import ij.plugin.PlugIn;
import ij.plugin.frame.RoiManager;
import ij.process.Blitter;
import ij.process.FloatPolygon;
import ij.process.ImageConverter;
import ij.process.ImageProcessor;
import ij.process.StackConverter;

/**
 * Main class implementing BOA plugin.
 * 
 * @author Richard Tyson
 * @author Till Bretschneider
 * @author Piotr Baniukiewicz
 */
public class BOA_ implements PlugIn {
  private static final Logger LOGGER = LoggerFactory.getLogger(BOA_.class.getName());

  /**
   * Indicate that {@link com.github.celldynamics.quimp.BOA_#runBoa(int, int)} is active.
   * 
   * <p>This method calls {@link com.github.celldynamics.quimp.ImageGroup#setIpSliceAll(int)} that
   * raises event
   * {@link com.github.celldynamics.quimp.BOA_.CustomStackWindow#updateSliceSelector()} which then
   * fire other methods.
   */
  boolean isSegRunning = false;

  /**
   * Used for breaking segmentation if Cancel is hit. If true segmentation is stopped.
   * 
   * @see #runBoa(int, int)
   */
  private boolean isSegBreakHit = false;

  /**
   * The canvas.
   */
  CustomCanvas canvas;

  /**
   * The window.
   */
  CustomStackWindow window;

  /**
   * The log area.
   */
  static TextArea logArea;

  /**
   * Indicate if the BOA plugin is run.
   */
  static boolean isBoaRunning = false;

  /**
   * The image group.
   */
  ImageGroup imageGroup;
  private Constrictor constrictor;
  private PluginFactory pluginFactory; // load and maintain plugins
  /**
   * Last selection tool selected in IJ.
   * 
   * <p>remember last tool to reselect it after truncating or
   * deleting operation
   */
  private String lastTool;

  /**
   * Reserved word that stands for plugin that is not selected.
   */
  public static final String NONE = "NONE";
  /**
   * Reserved word that states full view zoom in zoom choice. Also default text that appears there
   */
  private static final String fullZoom = "Frame zoom";
  /**
   * Hold current BOA object and provide access to only selected methods from plugin.
   * 
   * <p>Reference to this field is passed to plugins and give them possibility to call selected
   * methods from BOA class
   */
  public static ViewUpdater viewUpdater;
  /**
   * Keep data from getQuimPBuildInfo().
   * 
   * <p>These information are used in About dialog, window title bar, logging, etc. Static because
   * window related staff is in another classes.
   */
  public static QuimpVersion quimpInfo;
  private static int logCount; // add counter to logged messages
  /**
   * Number of Snake plugins available.
   */
  public static final int NUM_SNAKE_PLUGINS = 3;
  // private HistoryLogger historyLogger; // logger
  /**
   * Configuration object, available from all modules.
   * 
   * <p>Must be initialised here <b>AND</b> in constructor (to reset settings on next BOA call
   * without quitting Fiji) Keep data that will be serialized.
   */
  public static BOAState qState = new BOAState(null); // current state of BOA module

  /**
   * Main constructor.
   * 
   * <p>All static resources should be re-initialized here, otherwise they persist in memory between
   * subsequent BOA calls from Fiji.
   */
  public BOA_() {
    LOGGER.trace("Starting BOA from default constructor");
    qState = new BOAState(null);
    logCount = 1; // reset log count (it is also static)
  }

  /**
   * Main method called from Fiji. Initialises internal BOA structures.
   * 
   * @param arg Currently it can be string pointing to plugins directory
   * @see #setup(ImagePlus)
   */
  @Override
  public void run(final String arg) {
    if (IJ.versionLessThan("1.45")) {
      return;
    }

    if (BOA_.isBoaRunning) {
      BOA_.isBoaRunning = false;
      IJ.error("Warning: Only have one instance of BOA running at a time");
      return;
    }
    // assign current object to ViewUpdater
    viewUpdater = new ViewUpdater(this);
    // collect information about quimp version read from jar
    quimpInfo = QuimP.TOOL_VERSION;
    // create history logger
    // historyLogger = new HistoryLogger();

    // Build plugin engine
    try {
      String path;
      if (QuimP.PLUGIN_DIR == null) {
        path = IJ.getDirectory("plugins");
      } else {
        path = QuimP.PLUGIN_DIR;
      }
      if (path == null) {
        IJ.log("BOA: Plugin directory not found, use provided with arg: " + arg);
        LOGGER.debug("BOA: Plugin directory not found, use provided with arg: " + arg);
        path = arg;
      }
      // initialize plugin factory (jar scanning and registering)
      pluginFactory = PluginFactoryFactory.getPluginFactory(path);
    } catch (Exception e) {
      IJ.error("Error during initialisation of plugin engine", e.getMessage());
      LOGGER.debug(e.getMessage(), e);
      return;
    }

    ImagePlus ip = WindowManager.getCurrentImage();
    // Initialise arrays for plugins instances and give them initial values (GUI)
    qState = new BOAState(ip, pluginFactory, viewUpdater); // create BOA state machine
    if (IJ.getVersion().compareTo("1.46") < 0) {
      qState.boap.useSubPixel = false;
    } else {
      qState.boap.useSubPixel = true;
    }

    lastTool = IJ.getToolName();
    // stack or single image?
    if (ip == null || ip.getNChannels() > 1) {
      IJ.error("Single channel image required");
      return;
    } else if (ip.getStackSize() == 1) {
      qState.boap.singleImage = true;
    } else {
      qState.boap.singleImage = false;
    }
    // check if 8-bit image
    if (ip.getType() != ImagePlus.GRAY8) {
      YesNoCancelDialog ync =
              new YesNoCancelDialog(window, "Image bit depth", "8-bit Image required. Convert?");
      if (ync.yesPressed()) {
        if (qState.boap.singleImage) {
          new ImageConverter(ip).convertToGray8();
        } else {
          new StackConverter(ip).convertToGray8();
        }
      } else {
        return;
      }
    }
    BOA_.isBoaRunning = true;
    setup(ip); // create main objects in BOA and BOAState, build window + registration window

    if (qState.boap.useSubPixel == false) {
      BOA_.log("Upgrade to ImageJ 1.46, or higher," + "\nto get sub-pixel editing.");
    }
    if (IJ.getVersion().compareTo("1.49a") > 0) {
      BOA_.log("(ImageJ " + IJ.getVersion() + " untested)");
    }

    try {
      if (!qState.nest.isVacant()) {
        runBoa(1, 1);
      }
    } catch (BoaException be) {
      be.setMessageSinkType(MessageSinkTypes.GUI);
      be.handleException(IJ.getInstance(), "Inital preview failed");
    }
  }

  /**
   * Build all BOA windows and setup initial parameters for segmentation Define also
   * windowListener for cleaning after closing the main window by user.
   * 
   * @param ip Reference to image to be processed by BOA
   * @see BOAp
   */
  void setup(final ImagePlus ip) {
    if (qState.boap.paramsExist == null) {
      qState.segParam.setDefaults();
    }
    qState.boap.setup(ip);

    qState.nest = new Nest();
    imageGroup = new ImageGroup(ip, qState.nest);
    qState.boap.frame = 1;
    // build window and set its title
    canvas = new CustomCanvas(imageGroup.getOrgIpl());
    window = new CustomStackWindow(imageGroup.getOrgIpl(), canvas);
    window.buildWindow();
    window.setTitle(window.getTitle() + " :QuimP: " + quimpInfo.getVersion());
    // validate registered user
    new Registration(window, "QuimP Registration");
    // warn about scale - if it was adjusted in BOAState constructor
    if (qState.boap.isScaleAdjusted()) {
      BOA_.log("WARNING Scale was zero - set to 1");
    }
    if (qState.boap.isfIAdjusted()) {
      BOA_.log("WARNING Frame interval was zero - set to 1");
    }

    // adds window listener called on plugin closing
    window.addWindowListener(new CustomWindowAdapter());

    setScales(); // ask user for scales and set them
    updateImageScale();
    window.setScalesText();

    // check for ROIs - Use as cells
    new RoiManager(); // get open ROI manager, or create a new one
    RoiManager rm = RoiManager.getInstance();
    if (rm.getRoisAsArray().length != 0) {
      qState.nest.addHandlers(rm.getRoisAsArray(), 1);
    } else {
      BOA_.log("No cells from ROI manager");
      if (ip.getRoi() != null) {
        qState.nest.addHandler(ip.getRoi(), 1);
      } else {
        BOA_.log("No cells from selection");
      }
    }
    rm.close();
    ip.killRoi();

    constrictor = new Constrictor(); // does computations on snakes
  }

  /**
   * Display about information in BOA window. Called from menu bar. Reads also information from all
   * found plugins.
   */
  void about() {
    AboutDialog ad = new AboutDialog(window); // create about dialog with parent 'window'
    ad.appendLine(QuimpToolsCollection.getFormattedQuimPversion(quimpInfo)); // display template
    ad.appendLine("List of found plugins:");
    ad.appendDistance(); // type ----
    Map<String, PluginProperties> mp = pluginFactory.getRegisterdPlugins();
    // iterate over set
    for (Map.Entry<String, PluginProperties> entry : mp.entrySet()) {
      ad.appendLine("Plugin name: " + entry.getKey());
      ad.appendLine("   Plugin type: " + entry.getValue().getType());
      ad.appendLine("   Plugin path: " + entry.getValue().getFile().toString());
      ad.appendLine("   Plugin vers: " + entry.getValue().getVersion());
      // about is not stored in PluginProperties class due to optimization of memory
      ad.appendLine("   About (returned by plugin):");
      IQuimpCorePlugin tmpinst = pluginFactory.getInstance(entry.getKey());
      if (tmpinst != null) { // can be null on problem with instance
        String about = tmpinst.about(); // may return null
        if (about != null) {
          ad.appendLine(about);
        } else {
          ad.appendLine("Plugin does not provide about note");
        }
      }
      ad.appendDistance();
    }
    ad.setVisible(true); // must be after adding content
  }

  /**
   * Append string to log window in BOA plugin.
   * 
   * @param s String to display in BOA window
   */
  static void log(final String s) {
    if (logArea == null) {
      LOGGER.debug("[" + logCount++ + "] " + s + '\n');
    } else {
      logArea.append("[" + logCount++ + "] " + s + '\n');
    }
  }

  /**
   * Redraw current view. Process outlines by all active plugins. Do not run segmentation again
   * Updates liveSnake. Also disables UI.
   * 
   * <p>Strictly related to current view {@link BOAState.BOAp#frame}.
   */
  void recalculatePlugins() {
    LOGGER.trace("BOA: recalculatePlugins called");
    SnakeHandler sh;
    if (qState.nest.isVacant()) { // only update screen
      imageGroup.updateOverlay(qState.boap.frame);
      return;
    }
    imageGroup.updateToFrame(qState.boap.frame);
    try {
      for (int s = 0; s < qState.nest.size(); s++) { // for each snake
        sh = qState.nest.getHandler(s);
        if (sh.isSnakeHandlerFrozen()) {
          LOGGER.debug("SnakeHandler " + sh.getID() + " is frozen");
          continue;
        }
        if (qState.boap.frame < sh.getStartFrame()) {
          continue;
        }
        // but if one is on frame iplStack+n and strtFrame is e.g. 1 it may happen that there is
        // no continuity of this snake between frames. In this case getBackupSnake
        // returns null. In general QuimP assumes that if there is a cell on frame iplStack, it
        // will exist on all consecutive frames.
        Snake snake = sh.getBackupSnake(qState.boap.frame); // if exist get its backup copy
        // (segm)
        if (snake == null || !snake.alive) {
          continue;
        }
        try {
          Snake out = iterateOverSnakePlugins(snake); // apply all plugins to snake
          sh.storeThisSnake(out, qState.boap.frame); // set processed snake as final
        } catch (QuimpPluginException qpe) {
          // must be rewritten with whole runBOA #65 #67
          qpe.setMessageSinkType(MessageSinkTypes.IJERROR);
          BOA_.log(qpe.handleException(null, "Error in filter module"));
          sh.storeLiveSnake(qState.boap.frame); // so store only segmented snake as final
        } catch (BoaException be) { // less than 3 nodes in snake
          be.setMessageSinkType(MessageSinkTypes.IJERROR);
          BOA_.log(be.handleException(null, "Defective snake returned"));
        }
      }
    } catch (Exception e) {
      IJ.error("Plugin error", "Output snake may be defective. Reason: " + e.getMessage());
      LOGGER.debug(e.getMessage(), e);
    } finally {
      // historyLogger.addEntry("Plugin settings", qState);
      qState.store(qState.boap.frame); // always remember state of the BOA that is
    }
    imageGroup.updateOverlay(qState.boap.frame);
  }

  /**
   * Override action performed on window closing. Clear BOA._running static variable and prevent
   * to notify user that QuimP is running when it has been closed and called again.
   * 
   * <p>When user closes window by system button QuimP does not ask for saving current work. This is
   * because by default QuimP window is managed by ImageJ and it probably only hides it on closing
   * 
   * <p>This class could be located directly in CustomStackWindow which is included in BOA_. But it
   * needs to have access to BOA field <tt>running</tt>.
   * 
   * @author p.baniukiewicz
   */
  class CustomWindowAdapter extends WindowAdapter {

    /*
     * (non-Javadoc)
     * 
     * @see java.awt.event.WindowAdapter#windowClosed(java.awt.event.WindowEvent)
     */
    @Override
    // This method will be called when BOA_ window is closed already
    // It is too late for asking user
    public void windowClosed(final WindowEvent arg0) {
      LOGGER.trace("CLOSED");
      BOA_.isBoaRunning = false; // set marker
      qState.snakePluginList.clear(); // close all opened plugin windows
      if (qState.binarySegmentationPlugin != null) {
        qState.binarySegmentationPlugin.showUi(false);
      }
      canvas = null; // clear window data
      imageGroup = null;
      window = null;
      // clear static
      qState = null;
      viewUpdater = null;
    }

    /*
     * (non-Javadoc)
     * 
     * @see java.awt.event.WindowAdapter#windowClosing(java.awt.event.WindowEvent)
     */
    @Override
    public void windowClosing(final WindowEvent arg0) {
      LOGGER.trace("CLOSING");
    }

    /*
     * (non-Javadoc)
     * 
     * @see java.awt.event.WindowAdapter#windowActivated(java.awt.event.WindowEvent)
     */
    @Override
    public void windowActivated(final WindowEvent e) {
      LOGGER.trace("ACTIVATED");
      // rebuild menu for this local window
      // workaround for Mac and theirs menus on top screen bar
      // IJ is doing the same for activation of its window so every time one has correct menu
      // on top
      window.setMenuBar(window.menuBar);
    }
  }

  /**
   * Supports mouse actions on image at QuimP window according to selected option.
   * 
   * @author rtyson
   *
   */
  @SuppressWarnings("serial")
  class CustomCanvas extends ImageCanvas {

    /**
     * Empty constructor.
     * 
     * @param imp Reference to image loaded by BOA
     */
    CustomCanvas(final ImagePlus imp) {
      super(imp);
    }

    /**
     * @deprecated Actually not used in this version of QuimP.
     */
    @Override
    public void paint(final Graphics g) {
      super.paint(g);
      // int size = 80;
      // int screenSize = (int)(size*getMagnification());
      // int x = screenX(imageWidth/2 - size/2);
      // int y = screenY(imageHeight/2 - size/2);
      // g.setColor(Color.red);
      // g.drawOval(x, y, screenSize, screenSize);
    }

    /**
     * Implement mouse action on image loaded to BOA Used for manual editions of segmented
     * shape. Define reactions of mouse buttons according to GUI state, set by \b Delete and \b
     * Edit buttons.
     * 
     * @see BOAp
     * @see CustomStackWindow
     */
    @Override
    public void mousePressed(final MouseEvent e) {
      super.mousePressed(e);
      if (qState.boap.doDelete) {
        // BOA_.log("Delete at:
        // ("+offScreenX(e.getX())+","+offScreenY(e.getY())+")");
        deleteCell(offScreenX(e.getX()), offScreenY(e.getY()), qState.boap.frame);
        IJ.setTool(lastTool);
      }
      if (qState.boap.doFreeze) {
        freezeCell(offScreenX(e.getX()), offScreenY(e.getY()), qState.boap.frame);
      }
      if (qState.boap.doDeleteSeg) {
        // BOA_.log("Delete at:
        // ("+offScreenX(e.getX())+","+offScreenY(e.getY())+")");
        deleteSegmentation(offScreenX(e.getX()), offScreenY(e.getY()), qState.boap.frame);
      }
      if (qState.boap.editMode && qState.boap.editingID == -1) {
        // BOA_.log("Delete at:
        // ("+offScreenX(e.getX())+","+offScreenY(e.getY())+")");
        editSeg(offScreenX(e.getX()), offScreenY(e.getY()), qState.boap.frame);
      }
    }
  } // end of CustomCanvas

  /**
   * Extends standard ImageJ StackWindow adding own GUI elements.
   * 
   * <p>This class stands for definition of main BOA plugin GUI window. Current state of BOA plugin
   * is stored at {@link com.github.celldynamics.quimp.BOAState.BOAp} class.
   * 
   * @author rtyson
   * @see BOAp
   */
  @SuppressWarnings("serial")
  class CustomStackWindow extends StackWindow
          implements ActionListener, ItemListener, ChangeListener {

    /**
     * The Constant DEFAULT_SPINNER_SIZE.
     */
    static final int DEFAULT_SPINNER_SIZE = 5;

    /**
     * Number of currently supported plugins.
     */
    static final int SNAKE_PLUGIN_NUM = 3;
    /**
     * Any worker that run thread for boa or plugins will be referenced here.
     * 
     * @see #runBoaThread(int, int, boolean)
     * @see #populatePlugins(List)
     */
    private SwingWorker<Boolean, Object> sww = null;
    /**
     * Block rerun of runBoa() when spinners have been changed programmatically.
     * 
     * <p>Modification of spinners from code causes that stateChanged() event is called.
     */
    private boolean supressStateChangeBOArun = false;
    private Button bnSeg; // also play role of Cancel button
    private Button bnFinish;
    private Button bnLoad;
    private Button bnEdit;
    private Button bnQuit;
    private Button bnDefault;
    private Button bnScale;
    private Button bnCopyLast;
    private Button bnSave;

    private Button bnAdd;
    private Button bnDel;
    private Button bnDelSeg;
    private Button bnFreezeCell;

    private Checkbox cbPrevSnake;
    private Checkbox cbExpSnake;
    private Checkbox cbContractingDirection;
    private Checkbox cbPath;
    private Choice chZoom;

    /**
     * The log panel.
     */
    JScrollPane logPanel;

    private Label fpsLabel;
    private Label pixelLabel;
    private Label frameLabel;

    private JSpinner dsNodeRes;
    private JSpinner dsVelCrit;
    private JSpinner dsFImage;
    private JSpinner dsFCentral;
    private JSpinner dsFContract;
    private JSpinner dsFinalShrink;

    private JSpinner isMaxIterations;
    private JSpinner isBlowup;
    private JSpinner isSampletan;
    private JSpinner isSamplenorm;
    private Choice chFirstPluginName;
    private Choice chSecondPluginName;
    private Choice chThirdPluginName;
    private Button bnFirstPluginGUI;
    private Button bnSecondPluginGUI;
    private Button bnThirdPluginGUI;
    private Checkbox cbFirstPluginActiv;
    private Checkbox cbSecondPluginActiv;
    private Checkbox cbThirdPluginActiv;
    private Button bnPopulatePlugin; // same as menuPopulatePlugin
    private Button bnCopyLastPlugin;

    private MenuBar menuBar; // main menu bar
    private MenuItem menuAbout;
    private MenuItem menuOpenHelp;
    private MenuItem menuSaveConfig;
    private MenuItem menuLoadConfig;
    private MenuItem menuShowHistory;
    private MenuItem menuLoad;
    private MenuItem menuSave;
    private MenuItem menuSaveAs;
    private MenuItem menuDeletePlugin;
    private MenuItem menuApplyPlugin;
    private MenuItem menuSegmentationRun;
    private MenuItem menuSegmentationReset; // items
    private CheckboxMenuItem cbMenuPlotOriginalSnakes;
    private CheckboxMenuItem cbMenuPlotHead;

    private MenuItem menuPopulatePlugin;
    private CheckboxMenuItem cbMenuZoomFreeze;

    /**
     * Default constructor.
     * 
     * @param imp Image loaded to plugin
     * @param ic Image canvas
     */
    CustomStackWindow(final ImagePlus imp, final ImageCanvas ic) {
      super(imp, ic);

    }

    /**
     * Enables or disables all UI controls.
     * 
     * @param state true for enabled, false for disabled.
     */
    public void enableUi(boolean state) {
      bnSeg.setEnabled(state);
      bnFinish.setEnabled(state);
      bnLoad.setEnabled(state);
      bnEdit.setEnabled(state);
      bnQuit.setEnabled(state);
      bnDefault.setEnabled(state);
      bnScale.setEnabled(state);
      bnCopyLast.setEnabled(state);
      bnSave.setEnabled(state);

      bnAdd.setEnabled(state);
      bnDel.setEnabled(state);
      bnDelSeg.setEnabled(state);
      bnFreezeCell.setEnabled(state);

      cbPrevSnake.setEnabled(state);
      cbExpSnake.setEnabled(false); // disabled option
      cbContractingDirection.setEnabled(state);
      cbPath.setEnabled(state);
      chZoom.setEnabled(state);

      dsNodeRes.setEnabled(state);
      dsVelCrit.setEnabled(state);
      dsFImage.setEnabled(state);
      dsFCentral.setEnabled(state);
      dsFContract.setEnabled(state);
      dsFinalShrink.setEnabled(state);

      isMaxIterations.setEnabled(state);
      isBlowup.setEnabled(state);
      isSampletan.setEnabled(state);
      isSamplenorm.setEnabled(state);
      chFirstPluginName.setEnabled(state);
      chSecondPluginName.setEnabled(state);
      chThirdPluginName.setEnabled(state);

      if (chFirstPluginName.getSelectedItem() == NONE) {
        bnFirstPluginGUI.setEnabled(false);
        cbFirstPluginActiv.setEnabled(false);
      } else {
        bnFirstPluginGUI.setEnabled(state);
        cbFirstPluginActiv.setEnabled(state);
      }
      if (chSecondPluginName.getSelectedItem() == NONE) {
        bnSecondPluginGUI.setEnabled(false);
        cbSecondPluginActiv.setEnabled(false);
      } else {
        bnSecondPluginGUI.setEnabled(state);
        cbSecondPluginActiv.setEnabled(state);
      }
      if (chThirdPluginName.getSelectedItem() == NONE) {
        bnThirdPluginGUI.setEnabled(false);
        cbThirdPluginActiv.setEnabled(false);
      } else {
        bnThirdPluginGUI.setEnabled(state);
        cbThirdPluginActiv.setEnabled(state);
      }
      bnPopulatePlugin.setEnabled(state); // same as menuPopulatePlugin
      bnCopyLastPlugin.setEnabled(state);
      for (int i = 0; i < menuBar.getMenuCount(); i++) {
        menuBar.getMenu(i).setEnabled(state);
      }
    }

    /**
     * Similar to {@link #enableUi(boolean)} but always enables cancel button.
     * 
     * @param state true for enabled, false for disabled.
     */
    public void enableUiInterruptile(boolean state) {
      enableUi(state);
      bnSeg.setEnabled(true);
    }

    /**
     * Build user interface.
     * 
     * <p>This method is called as first. The interface is built in three steps: Left side of
     * window (configuration zone) and right side of main window (logs and other info and
     * buttons) and finally upper menubar
     * 
     * @see com.github.celldynamics.quimp.BOA_.CustomStackWindow#updateWindowState()
     */
    public void buildWindow() {

      setLayout(new BorderLayout(10, 3));

      if (!qState.boap.singleImage) {
        remove(sliceSelector);
      }
      if (!qState.boap.singleImage) {
        remove(this.getComponent(1)); // remove the play/pause button
      }
      Panel cp = buildControlPanel();
      Panel sp = buildSetupPanel();
      add(new Label(""), BorderLayout.NORTH);
      add(cp, BorderLayout.WEST); // add to the left, position 0
      add(ic, BorderLayout.CENTER);
      add(sp, BorderLayout.EAST);
      add(new Label(""), BorderLayout.SOUTH);

      LOGGER.debug("Menu: " + getMenuBar());
      menuBar = buildMenu(); // store menu in var to reuse on window activation
      setMenuBar(menuBar);
      pack();
      updateWindowState(); // window logic on start
    }

    /**
     * Build window menu.
     * 
     * <p>Menu is local for this window of QuimP and it is stored in \c quimpMenuBar variable. On
     * every time when QuimP is active, this menu is restored in
     * com.github.celldynamics.quimp.BOA_.CustomWindowAdapter.windowActivated(WindowEvent) method
     * This is due to overwriting menu by IJ on Mac (all menus are on top screen bar)
     * 
     * @return Reference to menu bar
     */
    final MenuBar buildMenu() {
      Menu menuHelp; // menu About in menubar
      Menu menuConfig; // menu Config in menubar
      Menu menuFile; // menu File in menubar
      Menu menuPlugin; // menu Plugin in menubar

      menuBar = new MenuBar();

      menuConfig = new Menu("Preferences");
      menuHelp = new Menu("Help");
      menuFile = new Menu("File");
      menuPlugin = new Menu("Plugin");
      Menu menuSegmentation; // menu Segmentation in menubar
      menuSegmentation = new Menu("Segmentation");

      // build main line
      menuBar.add(menuFile);
      menuBar.add(menuConfig);
      menuBar.add(menuPlugin);
      menuBar.add(menuSegmentation);
      menuBar.add(menuHelp);

      // add entries
      menuLoad = new MenuItem("Load experiment");
      menuLoad.addActionListener(this);
      menuFile.add(menuLoad);
      menuSave = new MenuItem("Save experiment");
      menuSave.addActionListener(this);
      menuFile.add(menuSave);
      menuSaveAs = new MenuItem("Save experiment as..");
      menuSaveAs.addActionListener(this);
      menuFile.add(menuSaveAs);

      menuFile.addSeparator();
      menuLoadConfig = new MenuItem("Load plugin preferences");
      menuLoadConfig.addActionListener(this);
      menuFile.add(menuLoadConfig);
      menuSaveConfig = new MenuItem("Save plugin preferences");
      menuSaveConfig.addActionListener(this);
      menuFile.add(menuSaveConfig);

      menuOpenHelp = new MenuItem("Help Contents");
      menuOpenHelp.addActionListener(this);
      menuHelp.add(menuOpenHelp);
      menuAbout = new MenuItem("About");
      menuAbout.addActionListener(this);
      menuHelp.add(menuAbout);

      cbMenuPlotOriginalSnakes = new CheckboxMenuItem("Plot original");
      cbMenuPlotOriginalSnakes.setState(qState.boap.isProcessedSnakePlotted);
      cbMenuPlotOriginalSnakes.addItemListener(this);
      menuConfig.add(cbMenuPlotOriginalSnakes);
      cbMenuPlotHead = new CheckboxMenuItem("Plot head");
      cbMenuPlotHead.setState(qState.boap.isHeadPlotted);
      cbMenuPlotHead.addItemListener(this);
      menuConfig.add(cbMenuPlotHead);
      cbMenuZoomFreeze = new CheckboxMenuItem("Zoom freezes");
      cbMenuZoomFreeze.setState(qState.boap.isZoomFreeze);
      cbMenuZoomFreeze.addItemListener(this);
      menuConfig.add(cbMenuZoomFreeze);

      menuShowHistory = new MenuItem("Show history");
      menuShowHistory.addActionListener(this);
      menuConfig.add(menuShowHistory);

      menuDeletePlugin = new MenuItem("Discard all");
      menuDeletePlugin.addActionListener(this);
      menuPlugin.add(menuDeletePlugin);
      menuApplyPlugin = new MenuItem("Re-apply all");
      menuApplyPlugin.addActionListener(this);
      menuPlugin.add(menuApplyPlugin);
      menuPopulatePlugin = new MenuItem("Populate to all frames");
      menuPopulatePlugin.addActionListener(this);
      menuPlugin.add(menuPopulatePlugin);

      menuSegmentationRun = new MenuItem("Binary segmentation");
      menuSegmentationRun.addActionListener(this);
      menuSegmentationReset = new MenuItem("Clear all");
      menuSegmentationReset.addActionListener(this);
      menuSegmentation.add(menuSegmentationRun);
      menuSegmentation.add(menuSegmentationReset);

      return menuBar;
    }

    /**
     * Build right side of main BOA window.
     * 
     * @return Reference to panel
     */
    final Panel buildSetupPanel() {
      Panel setupPanel = new Panel(); // Main panel comprised from North, Centre and South subpanels
      Panel northPanel = new Panel(); // Contains static info and four buttons (Scale, Truncate, etc
      Panel southPanel = new Panel(); // Quit and Finish
      Panel centerPanel = new Panel();
      Panel pluginPanelButtons = new Panel(); // buttons below plugins

      setupPanel.setLayout(new BorderLayout());
      northPanel.setLayout(new GridLayout(4, 2));
      southPanel.setLayout(new GridLayout(2, 2));
      centerPanel.setLayout(new BoxLayout(centerPanel, BoxLayout.PAGE_AXIS));

      // plugins buttons
      pluginPanelButtons.setLayout(new GridLayout(1, 2)); // here is number of buttons
      bnPopulatePlugin = addButton("Populate fwd", pluginPanelButtons);
      bnCopyLastPlugin = addButton("Copy prev", pluginPanelButtons);

      // Grid bag for plugin zone
      GridBagLayout gridbag = new GridBagLayout();
      GridBagConstraints c = new GridBagConstraints();
      c.weightx = 0.5;
      c.fill = GridBagConstraints.HORIZONTAL;
      c.anchor = GridBagConstraints.LINE_START;
      Panel pluginPanel = new Panel();
      pluginPanel.setLayout(gridbag);

      fpsLabel = new Label("F Interval: " + IJ.d2s(qState.boap.getImageFrameInterval(), 3) + " s");
      northPanel.add(fpsLabel);
      pixelLabel = new Label("Scale: " + IJ.d2s(qState.boap.getImageScale(), 6) + " \u00B5m");
      northPanel.add(pixelLabel);

      bnScale = addButton("Set Scale", northPanel);
      bnDelSeg = addButton("Truncate Seg", northPanel);
      bnAdd = addButton("Add cell", northPanel);
      bnDel = addButton("Delete cell", northPanel);
      bnFreezeCell = addButton("Freeze", northPanel);

      // build subpanel with plugins
      // get plugins names collected by PluginFactory
      ArrayList<String> pluginList =
              qState.snakePluginList.getPluginNames(IQuimpCorePlugin.DOES_SNAKES);
      // add NONE to list
      pluginList.add(0, NONE);

      // plugins are recognized by their names returned from pluginFactory.getPluginNames() so
      // if there is no names, it is not possible to call nonexisting plugins, because calls
      // are made using plugin names. see actionPerformed. If plugin of given name (NONE) is
      // not found getInstance return null which is stored in SnakePluginList and checked
      // during run
      // default values for plugin activity are stored in SnakePluginList
      chFirstPluginName = addComboBox(pluginList.toArray(new String[0]), pluginPanel);
      c.gridx = 0;
      c.gridy = 0;
      pluginPanel.add(chFirstPluginName, c);
      bnFirstPluginGUI = addButton("GUI", pluginPanel);
      c.gridx = 1;
      c.gridy = 0;
      pluginPanel.add(bnFirstPluginGUI, c);
      cbFirstPluginActiv = addCheckbox("A", pluginPanel, qState.snakePluginList.isActive(0));
      c.gridx = 2;
      c.gridy = 0;
      pluginPanel.add(cbFirstPluginActiv, c);

      chSecondPluginName = addComboBox(pluginList.toArray(new String[0]), pluginPanel);
      c.gridx = 0;
      c.gridy = 1;
      pluginPanel.add(chSecondPluginName, c);
      bnSecondPluginGUI = addButton("GUI", pluginPanel);
      c.gridx = 1;
      c.gridy = 1;
      pluginPanel.add(bnSecondPluginGUI, c);
      cbSecondPluginActiv = addCheckbox("A", pluginPanel, qState.snakePluginList.isActive(1));
      c.gridx = 2;
      c.gridy = 1;
      pluginPanel.add(cbSecondPluginActiv, c);

      chThirdPluginName = addComboBox(pluginList.toArray(new String[0]), pluginPanel);
      c.gridx = 0;
      c.gridy = 2;
      pluginPanel.add(chThirdPluginName, c);
      bnThirdPluginGUI = addButton("GUI", pluginPanel);
      c.gridx = 1;
      c.gridy = 2;
      pluginPanel.add(bnThirdPluginGUI, c);
      cbThirdPluginActiv = addCheckbox("A", pluginPanel, qState.snakePluginList.isActive(2));
      c.gridx = 2;
      c.gridy = 2;
      pluginPanel.add(cbThirdPluginActiv, c);

      c.gridx = 0;
      c.gridy = 3;
      c.gridwidth = 3;
      c.fill = GridBagConstraints.HORIZONTAL;
      pluginPanel.add(pluginPanelButtons, c);

      // --------build log---------
      Panel tp = new Panel(); // panel with text area
      tp.setLayout(new GridLayout(1, 1));
      logArea = new TextArea(15, 15);
      logArea.setEditable(false);
      tp.add(logArea);
      logPanel = new JScrollPane(tp);

      // ------------------------------

      // --------build south--------------
      southPanel.add(new Label("")); // blankes
      southPanel.add(new Label("")); // blankes
      bnQuit = addButton("Quit", southPanel);
      bnFinish = addButton("Save & Quit", southPanel);
      // ------------------------------

      centerPanel.add(new Label("Snake Plugins:"));
      centerPanel.add(pluginPanel);
      centerPanel.add(new Label("Logs:"));
      centerPanel.add(logPanel);
      setupPanel.add(northPanel, BorderLayout.PAGE_START);
      setupPanel.add(centerPanel, BorderLayout.CENTER);
      setupPanel.add(southPanel, BorderLayout.PAGE_END);

      if (pluginList.isEmpty()) {
        BOA_.log("No plugins found");
      } else {
        BOA_.log("Found " + (pluginList.size() - 1) + " plugins (see About)");
      }

      return setupPanel;
    }

    /**
     * Build left side of main BOA window.
     * 
     * @return Reference to built panel
     */
    final Panel buildControlPanel() {
      Panel controlPanel = new Panel();
      Panel topPanel = new Panel();
      Panel paramPanel = new Panel();
      Panel bottomPanel = new Panel();

      controlPanel.setLayout(new BorderLayout());
      topPanel.setLayout(new GridLayout(2, 2));
      paramPanel.setLayout(new GridLayout(15, 1));
      bottomPanel.setLayout(new GridLayout(1, 2));

      // --------build topPanel--------
      bnLoad = addButton("Load", topPanel);
      bnSave = addButton("Save", topPanel);
      bnCopyLast = addButton("Copy prev", topPanel);
      bnDefault = addButton("Default", topPanel);

      // -----------------------

      // --------build paramPanel--------------
      dsNodeRes = addDoubleSpinner("Node Spacing:", paramPanel, qState.segParam.getNodeRes(), 1.,
              20., 0.2, CustomStackWindow.DEFAULT_SPINNER_SIZE);
      isMaxIterations = addIntSpinner("Max Iterations:", paramPanel, qState.segParam.max_iterations,
              100, 10000, 100, CustomStackWindow.DEFAULT_SPINNER_SIZE);
      isBlowup = addIntSpinner("Blowup:", paramPanel, qState.segParam.blowup, -200, 200, 1,
              CustomStackWindow.DEFAULT_SPINNER_SIZE);
      dsVelCrit = addDoubleSpinner("Crit velocity:", paramPanel, qState.segParam.vel_crit, -2, 2.,
              0.001, CustomStackWindow.DEFAULT_SPINNER_SIZE);
      dsFImage = addDoubleSpinner("Image F:", paramPanel, qState.segParam.f_image, -10.0, 10.0,
              0.01, CustomStackWindow.DEFAULT_SPINNER_SIZE);
      dsFCentral = addDoubleSpinner("Central F:", paramPanel, qState.segParam.f_central, -1, 1,
              0.002, CustomStackWindow.DEFAULT_SPINNER_SIZE);
      dsFContract = addDoubleSpinner("Contract F:", paramPanel, qState.segParam.f_contract, -1, 1,
              0.001, CustomStackWindow.DEFAULT_SPINNER_SIZE);
      dsFinalShrink = addDoubleSpinner("Final Shrink:", paramPanel, qState.segParam.finalShrink,
              -100, 100, 0.5, CustomStackWindow.DEFAULT_SPINNER_SIZE);
      isSampletan = addIntSpinner("Sample tan:", paramPanel, qState.segParam.sample_tan, 1, 30, 1,
              CustomStackWindow.DEFAULT_SPINNER_SIZE);
      isSamplenorm = addIntSpinner("Sample norm:", paramPanel, qState.segParam.sample_norm, 1, 60,
              1, CustomStackWindow.DEFAULT_SPINNER_SIZE);

      cbPrevSnake =
              addCheckbox("Use Previous Snake", paramPanel, qState.segParam.use_previous_snake);
      cbExpSnake = addCheckbox("Expanding Snake", paramPanel, qState.segParam.expandSnake);
      cbExpSnake.setEnabled(false); // FIXME DISABLED OPTION
      cbContractingDirection =
              addCheckbox("Contracing Snake", paramPanel, qState.segParam.contractingDirection);

      Panel segEditPanel = new Panel();
      segEditPanel.setLayout(new GridLayout(1, 2));
      bnSeg = addButton("SEGMENT", segEditPanel);
      bnEdit = addButton("Edit", segEditPanel);
      paramPanel.add(segEditPanel);

      // mini panel comprised from slice selector and frame number (if not single image)
      Panel sliderPanel = new Panel();
      sliderPanel.setLayout(new FlowLayout(FlowLayout.LEFT));

      if (!qState.boap.singleImage) {
        sliceSelector.setPreferredSize(new Dimension(165, 20));
        sliceSelector.addAdjustmentListener(this);
        sliderPanel.add(sliceSelector);
        // frame number on right of slice selector
        frameLabel = new Label(imageGroup.getOrgIpl().getSlice() + "  ");
        sliderPanel.add(frameLabel);
      }
      paramPanel.add(sliderPanel);
      // ----------------------------------

      // -----build bottom panel---------
      cbPath = addCheckbox("Show paths", bottomPanel, qState.segParam.showPaths);
      chZoom = addComboBox(new String[] { fullZoom }, bottomPanel);
      // add mouse listener to create menu dynamically on click
      chZoom.addMouseListener(new MouseAdapter() {
        @Override
        public void mousePressed(MouseEvent e) {
          LOGGER.trace("EVENT:mousePressed");
          fillZoomChoice();
        }
      });
      // -------------------------------
      // build control panel

      controlPanel.add(topPanel, BorderLayout.NORTH);
      controlPanel.add(paramPanel, BorderLayout.CENTER);
      controlPanel.add(bottomPanel, BorderLayout.SOUTH);

      return controlPanel;
    }

    /**
     * Rebuild Zoom choice UI according to cells on current frame {@link BOAp#frame}.
     * 
     * <p>According to #193 if there is no cell left it creates empty entry and set it selected to
     * enforce user to set explicitly default unzoom value and fire itemStateChanged.
     */
    private void fillZoomChoice() {
      String prev = chZoom.getSelectedItem();
      LOGGER.trace(prev);
      chZoom.removeAll();
      chZoom.add(fullZoom); // default word for full zoom (100% of view)
      List<Integer> frames = qState.nest.getSnakesforFrame(qState.boap.frame);
      for (Integer i : frames) {
        chZoom.add(i.toString());
      }
      if (chZoom.getItemCount() == 1) { // dirty trick to enforce triggering itemStateChanged (#193)
        chZoom.add("");
        chZoom.select("");
      } else {
        chZoom.select(prev); // select last selected (if exists)
      }
    }

    /**
     * Helper method for adding buttons to UI. Creates UI element and adds it to panel
     * 
     * @param label Label on button
     * @param p Reference to the panel where button is located
     * @return Reference to created button
     */
    private Button addButton(final String label, final Container p) {
      Button b = new Button(label);
      b.addActionListener(this);
      p.add(b);
      return b;
    }

    /**
     * Helper method for creating checkbox in UI.
     * 
     * @param label Label of checkbox
     * @param p Reference to the panel where checkbox is located
     * @param d Initial state of checkbox
     * @return Reference to created checkbox
     */
    private Checkbox addCheckbox(final String label, final Container p, boolean d) {
      Checkbox c = new Checkbox(label, d);
      c.addItemListener(this);
      p.add(c);
      return c;
    }

    /**
     * Helper method for creating ComboBox in UI. Creates UI element and adds it to panel
     *
     * @param s Strings to be included in ComboBox
     * @param mp Reference to the panel where ComboBox is located
     * @return Reference to created ComboBox
     */
    private Choice addComboBox(final String[] s, final Container mp) {
      Choice c = new Choice();
      for (String st : s) {
        c.add(st);
      }
      c.select(0);
      c.addItemListener(this);
      mp.add(c);
      return c;
    }

    /**
     * Helper method for creating spinner in UI with real values.
     * 
     * @param s Label of spinner (added on its left side)
     * @param mp Reference of panel where spinner is located
     * @param d The current vale of model
     * @param min The first number in sequence
     * @param max The last number in sequence
     * @param step The difference between numbers in sequence
     * @param columns The number of columns preferred for display
     * @return Reference to created spinner
     */
    private JSpinner addDoubleSpinner(final String s, final Container mp, double d, double min,
            double max, double step, int columns) {
      SpinnerNumberModel model = new SpinnerNumberModel(d, min, max, step);
      JSpinner spinner = new JSpinner(model);
      ((JSpinner.DefaultEditor) spinner.getEditor()).getTextField().setColumns(columns);
      spinner.addChangeListener(this);

      Panel p = new Panel();
      p.setLayout(new FlowLayout(FlowLayout.RIGHT));
      Label label = new Label(s);
      p.add(label);
      p.add(spinner);
      mp.add(p);
      return spinner;
    }

    /**
     * Helper method for creating spinner in UI with integer values.
     * 
     * @param s Label of spinner (added on its left side)
     * @param mp Reference of panel where spinner is located
     * @param d The current vale of model
     * @param min The first number in sequence
     * @param max The last number in sequence
     * @param step The difference between numbers in sequence
     * @param columns The number of columns preferred for display
     * @return Reference to created spinner
     */
    private JSpinner addIntSpinner(final String s, final Container mp, int d, int min, int max,
            int step, int columns) {
      SpinnerNumberModel model = new SpinnerNumberModel(d, min, max, step);
      JSpinner spinner = new JSpinner(model);
      ((JSpinner.DefaultEditor) spinner.getEditor()).getTextField().setColumns(columns);
      spinner.addChangeListener(this);

      Panel p = new Panel();
      p.setLayout(new FlowLayout(FlowLayout.RIGHT));
      Label label = new Label(s);
      p.add(label);
      p.add(spinner);
      mp.add(p);
      return spinner;
    }

    /**
     * Set default values defined in model class {@link com.github.celldynamics.quimp.BOAState.BOAp}
     * and update UI.
     * 
     * @see BOAp
     */
    private void setDefualts() {
      qState.segParam.setDefaults();
      updateSpinnerValues();
      cbContractingDirection.setState(qState.segParam.contractingDirection);
    }

    /**
     * Update spinners in BOA UI Update spinners according to values stored in machine state
     * {@link com.github.celldynamics.quimp.BOAState.BOAp}.
     * 
     * @see BOAp
     */
    private void updateSpinnerValues() {
      // block rerun of runBoa() that is called on Spinner event
      supressStateChangeBOArun = true;
      dsNodeRes.setValue(qState.segParam.getNodeRes());
      dsVelCrit.setValue(qState.segParam.vel_crit);
      dsFImage.setValue(qState.segParam.f_image);
      dsFCentral.setValue(qState.segParam.f_central);
      dsFContract.setValue(qState.segParam.f_contract);
      dsFinalShrink.setValue(qState.segParam.finalShrink);
      isMaxIterations.setValue(qState.segParam.max_iterations);
      isBlowup.setValue(qState.segParam.blowup);
      isSampletan.setValue(qState.segParam.sample_tan);
      isSamplenorm.setValue(qState.segParam.sample_norm);
      supressStateChangeBOArun = false;
    }

    /**
     * Update checkboxes.
     * 
     * @see com.github.celldynamics.quimp.SnakePluginList
     * @see #itemStateChanged(ItemEvent)
     */
    private void updateCheckBoxes() {
      // first plugin activity
      cbFirstPluginActiv.setState(qState.snakePluginList.isActive(0));
      // second plugin activity
      cbSecondPluginActiv.setState(qState.snakePluginList.isActive(1));
      // third plugin activity
      cbThirdPluginActiv.setState(qState.snakePluginList.isActive(2));
    }

    /**
     * Update Menu checkboxes.
     */
    private void updateMenus() {
      cbMenuPlotOriginalSnakes.setState(qState.boap.isProcessedSnakePlotted);
      cbMenuPlotHead.setState(qState.boap.isHeadPlotted);
      cbMenuZoomFreeze.setState(qState.boap.isZoomFreeze);
    }

    /**
     * Update static fileds on window.
     */
    private void updateStatics() {
      setScalesText();
    }

    /**
     * Update Choices.
     * 
     * <p>This method is called from CustomStackWindow.itemStateChanged(ItemEvent) to update colors
     * of Choices.
     * 
     * @see com.github.celldynamics.quimp.SnakePluginList
     * @see #itemStateChanged(ItemEvent)
     */
    private void updateChoices() {
      final Color ok = new Color(178, 255, 102);
      final Color bad = new Color(255, 153, 153);
      // first slot snake plugin
      if (qState.snakePluginList.getName(0).isEmpty()) {
        chFirstPluginName.select(NONE);
        chFirstPluginName.setBackground(null);
      } else {
        // try to select name from pluginList in choice
        chFirstPluginName.select(qState.snakePluginList.getName(0));
        // tried selecting but still on none - it means that plugin name from snkePluginList is not
        // on choice list. Tis may happen when choice is propagated from directory
        // butsnakePluginList from external QCONF
        if (chFirstPluginName.getSelectedItem().equals(NONE)) {
          chFirstPluginName.add(qState.snakePluginList.getName(0)); // add to list
          chFirstPluginName.setBackground(bad); // set as bad
        } else if (qState.snakePluginList.getInstance(0) == null) {
          // WARN does not check instance(0) is the instance of getName(0)
          chFirstPluginName.setBackground(bad);
        } else {
          chFirstPluginName.setBackground(ok);
        }
      }
      // second slot snake plugin
      if (qState.snakePluginList.getName(1).isEmpty()) {
        chSecondPluginName.select(NONE);
        chSecondPluginName.setBackground(null);
      } else {
        chSecondPluginName.select(qState.snakePluginList.getName(1));
        if (chSecondPluginName.getSelectedItem().equals(NONE)) {
          chSecondPluginName.add(qState.snakePluginList.getName(1)); // add to list
          chSecondPluginName.setBackground(bad); // set as bad
        } else if (qState.snakePluginList.getInstance(1) == null) {
          chSecondPluginName.setBackground(bad);
        } else {
          chSecondPluginName.setBackground(ok);
        }
      }
      // third slot snake plugin
      if (qState.snakePluginList.getName(2).isEmpty()) {
        chThirdPluginName.select(NONE);
        chThirdPluginName.setBackground(null);
      } else {
        chThirdPluginName.select(qState.snakePluginList.getName(2));
        if (chThirdPluginName.getSelectedItem().equals(NONE)) {
          chThirdPluginName.add(qState.snakePluginList.getName(2)); // add to list
          chThirdPluginName.setBackground(bad); // set as bad
        } else if (qState.snakePluginList.getInstance(2) == null) {
          chThirdPluginName.setBackground(bad);
        } else {
          chThirdPluginName.setBackground(ok);
        }
      }
      // zoom choice
      if (qState.boap.snakeToZoom > -1) {
        chZoom.select(String.valueOf(qState.boap.snakeToZoom));
      }

    }

    /**
     * Implement user interface logic.
     * 
     * <p>Do not refresh values, rather disable/enable controls.
     */
    private void updateWindowState() {
      updateCheckBoxes(); // update checkboxes
      updateChoices(); // and choices
      updateStatics();

      // Rule 1 - NONE on any slot in filters disable GUI button and Active checkbox but only if
      // there is no worker working
      if (sww == null || sww.getState() == SwingWorker.StateValue.DONE) {
        if (chFirstPluginName.getSelectedItem() == NONE) {
          cbFirstPluginActiv.setEnabled(false);
          bnFirstPluginGUI.setEnabled(false);
        } else {
          cbFirstPluginActiv.setEnabled(true);
          bnFirstPluginGUI.setEnabled(true);
        }
        if (chSecondPluginName.getSelectedItem() == NONE) {
          cbSecondPluginActiv.setEnabled(false);
          bnSecondPluginGUI.setEnabled(false);
        } else {
          cbSecondPluginActiv.setEnabled(true);
          bnSecondPluginGUI.setEnabled(true);
        }
        if (chThirdPluginName.getSelectedItem() == NONE) {
          cbThirdPluginActiv.setEnabled(false);
          bnThirdPluginGUI.setEnabled(false);
        } else {
          cbThirdPluginActiv.setEnabled(true);
          bnThirdPluginGUI.setEnabled(true);
        }
      }

    }

    /**
     * Main method that handles all actions performed on UI elements.
     * 
     * <p>Do not support mouse events, only UI elements like buttons, spinners and menus. Runs also
     * main algorithm on specified input state and update screen on plugins operations.
     * 
     * @param e Type of event
     * @see com.github.celldynamics.quimp.BOAState.BOAp
     * @see com.github.celldynamics.quimp.BOA_.CustomStackWindow#updateWindowState()
     */
    @Override
    public void actionPerformed(final ActionEvent e) {
      LOGGER.trace("EVENT:actionPerformed");
      boolean run = false; // some actions require to re-run segmentation. They set it to true
      Object b = e.getSource();
      if (b == bnDel && !qState.boap.editMode && !qState.boap.doDeleteSeg
              && !qState.boap.doFreeze) {
        if (qState.boap.doDelete == false) {
          bnDel.setLabel("*STOP DEL*");
          qState.boap.doDelete = true;
          lastTool = IJ.getToolName();
          IJ.setTool(Toolbar.LINE);
        } else {
          qState.boap.doDelete = false;
          bnDel.setLabel("Delete cell");
          IJ.setTool(lastTool);
        }
        return;
      }
      if (qState.boap.doDelete) { // stop if delete is on
        BOA_.log("**DELETE IS ON**");
        return;
      }
      if (b == bnFreezeCell && !qState.boap.editMode && !qState.boap.doDeleteSeg
              && !qState.boap.doDelete) {
        setFreeze(!qState.boap.doFreeze);
        return;
      }
      if (qState.boap.doFreeze) { // stop if delete is on
        BOA_.log("**FREEZE IS ON**");
        return;
      }

      if (b == bnDelSeg && !qState.boap.editMode) {
        if (!qState.boap.doDeleteSeg) {
          bnDelSeg.setLabel("*STOP TRUNCATE*");
          qState.boap.doDeleteSeg = true;
          lastTool = IJ.getToolName();
          IJ.setTool(Toolbar.LINE);
        } else {
          qState.boap.doDeleteSeg = false;
          bnDelSeg.setLabel("Truncate Seg");
          IJ.setTool(lastTool);
        }
        return;
      }
      if (qState.boap.doDeleteSeg) { // stop if delete is on
        BOA_.log("**TRUNCATE SEG IS ON**");
        return;
      }
      if (b == bnEdit) {
        if (qState.boap.editMode == false) {
          bnEdit.setLabel("*STOP EDIT*");
          BOA_.log("**EDIT IS ON**");
          qState.boap.editMode = true;
          lastTool = IJ.getToolName();
          IJ.setTool(Toolbar.LINE);
          if (qState.nest.size() == 1) {
            editSeg(0, 0, qState.boap.frame); // if only 1 snake go straight to edit, if
          }
          // more user must pick one
          // remember that this frame is edited
          qState.storeOnlyEdited(qState.boap.frame);
        } else {
          qState.boap.editMode = false;
          if (qState.boap.editingID != -1) {
            stopEdit();
          }
          bnEdit.setLabel("Edit");
          IJ.setTool(lastTool);
        }
        return;
      }
      if (qState.boap.editMode) { // stop if edit on
        BOA_.log("**EDIT IS ON**");
        return;
      }
      if (b == bnDefault) { // run in thread
        this.setDefualts();
        run = true;
      } else if (b == bnSeg) { // main segmentation procedure starts here
        if (sww != null && sww.getState() != SwingWorker.StateValue.DONE) {
          // if any worker works
          isSegBreakHit = true;
          return;
        }
        runBoaThread(qState.boap.frame, qState.boap.getFrames(), true);
      } else if (b == bnScale) {
        setScales();
        pixelLabel.setText("Scale: " + IJ.d2s(qState.boap.getImageScale(), 6) + " \u00B5m");
        fpsLabel.setText("F Interval: " + IJ.d2s(qState.boap.getImageFrameInterval(), 3) + " s");
      } else if (b == bnAdd) {
        addCell(canvas.getImage().getRoi(), qState.boap.frame);
        canvas.getImage().killRoi();
      } else if (b == bnFinish) {
        fpsLabel.setName("moo");
        finish(true); // ask for new file
        quit();
      } else if (b == bnQuit) {
        YesNoCancelDialog ync;
        ync = new YesNoCancelDialog(window, "Quit", "Quit without saving?");
        if (!ync.yesPressed()) {
          return;
        }
        quit();
      } else if (b == bnSave || b == menuSave) {
        finish(false); // update old file
      }
      if (b == menuSaveAs) {
        finish(true); // create new file
      }
      // process plugin GUI buttons
      if (b == bnFirstPluginGUI) {
        LOGGER.debug("First plugin GUI, state of BOAp is " + qState.snakePluginList.getInstance(0));
        if (qState.snakePluginList.getInstance(0) != null) {
          qState.snakePluginList.getInstance(0).showUi(true);
        }
      }
      if (b == bnSecondPluginGUI) {
        LOGGER.debug(
                "Second plugin GUI, state of BOAp is " + qState.snakePluginList.getInstance(1));
        if (qState.snakePluginList.getInstance(1) != null) {
          qState.snakePluginList.getInstance(1).showUi(true);
        }
      }
      if (b == bnThirdPluginGUI) {
        LOGGER.debug("Third plugin GUI, state of BOAp is " + qState.snakePluginList.getInstance(2));
        if (qState.snakePluginList.getInstance(2) != null) {
          qState.snakePluginList.getInstance(2).showUi(true);
        }
      }
      if (b == bnCopyLastPlugin) { // run in thread
        int frameCopyFrom = qState.boap.frame - 1;
        if (frameCopyFrom < 1 || frameCopyFrom > qState.boap.getFrames()) {
          return;
        }
        LOGGER.debug(
                "Copy config from frame " + frameCopyFrom + " current frame " + qState.boap.frame);
        qState.copyPluginListFromSnapshot(frameCopyFrom);
        setBusyStatus(true, false);
        recalculatePlugins();
        setBusyStatus(false, true); // update screen
      }

      if (b == bnCopyLast) { // copy previous settings
        int frameCopyFrom = qState.boap.frame - 1;
        if (frameCopyFrom < 1 || frameCopyFrom > qState.boap.getFrames()) {
          return;
        }
        LOGGER.debug(
                "Copy config from frame " + frameCopyFrom + " current frame " + qState.boap.frame);
        qState.copySegParamFromSnapshot(frameCopyFrom);
        updateSpinnerValues(); // update segmentation gui
        run = true; // re run BOA (+plugins)
      }
      if (b == bnPopulatePlugin) { // copy plugin stack forward
        List<Integer> range = IntStream.rangeClosed(qState.boap.frame + 1, qState.boap.getFrames())
                .boxed().collect(Collectors.toList());
        populatePlugins(range);
      }
      // menu listeners
      if (b == menuAbout) {
        about();
      }
      if (b == menuOpenHelp) {
        String url = new PropertyReader().readProperty("quimpconfig.properties", "manualURL");
        try {
          java.awt.Desktop.getDesktop().browse(new URI(url));
        } catch (Exception e1) {
          IJ.error("Could not open help", e1.getMessage());
          LOGGER.debug(e1.getMessage(), e1);
        }
        return;
      }
      if (b == menuSaveConfig) {
        String saveIn = qState.boap.getOutputFileCore().getParent();
        saveIn = (saveIn == null) ? System.getProperty("user.dir") : saveIn;
        SaveDialog sd = new SaveDialog("Save plugin config data...", saveIn,
                qState.boap.getFileName(), FileExtensions.pluginFileExt);
        if (sd.getFileName() != null) {
          try {
            // Create Serialization object with extra info layer
            Serializer<SnakePluginList> s;
            s = new Serializer<>(qState.snakePluginList, quimpInfo);
            s.setPretty(); // set pretty format
            s.save(sd.getDirectory() + sd.getFileName()); // save it
            s = null; // remove
          } catch (FileNotFoundException e1) {
            IJ.error("Problem with saving plugin config", e1.getMessage());
            LOGGER.debug(e1.getMessage(), e1);
          }
        }
      }

      /*
       * Loads configuration of current filter stack.
       * 
       * @see <a href=
       * "http://www.trac-wsbc.linkpc.net:8080/trac/QuimP/ticket/155">http://www.trac-wsbc.linkpc.
       * net:8080/trac/QuimP/ticket/155</a>
       */
      if (b == menuLoadConfig) {
        OpenDialog od = new OpenDialog("Load plugin config data...", "");
        if (od.getFileName() != null) {
          try {
            Serializer<SnakePluginList> loaded; // loaded instance
            // create serializer
            Serializer<SnakePluginList> s =
                    new Serializer<>(SnakePluginList.class, QuimP.TOOL_VERSION);
            s.registerConverter(new Converter170202<>(QuimP.TOOL_VERSION));
            // pass data to constructor of serialized object. Those data are not
            // serialized and must be passed externally
            s.registerInstanceCreator(SnakePluginList.class,
                    new SnakePluginListInstanceCreator(3, pluginFactory, viewUpdater));
            loaded = s.load(od.getDirectory() + od.getFileName());
            // restore loaded objects
            qState.snakePluginList.clear(); // closes windows, etc
            qState.snakePluginList = loaded.obj; // replace with fresh instance
            qState.store(qState.boap.frame); // copy loaded snakePluginList to snapshots
            // commented after #155
            /*
             * YesNoCancelDialog yncd = new YesNoCancelDialog(IJ.getInstance(),
             * "Warning", "Would you like to load this configuration for all frames?");
             * if (yncd.yesPressed()) { // propagate over all frames for (int i = 0; i <
             * qState.snakePluginListSnapshots.size(); i++) { if (i == qState.boap.frame
             * - 1) continue; // do not copy itself
             * qState.snakePluginListSnapshots.set(i,
             * qState.snakePluginList.getDeepCopy()); } }
             */
            recalculatePlugins(); // update screen
          } catch (IOException e1) {
            IJ.error("Problem with loading plugin config", e1.getMessage());
            LOGGER.debug(e1.getMessage(), e1);
          } catch (JsonSyntaxException e1) {
            IJ.error("Problem with configuration file", e1.getMessage());
            LOGGER.debug(e1.getMessage(), e1);
          } catch (Exception e1) {
            IJ.error("Error", "File can not be loaded or parsed" + e1.getMessage());
            LOGGER.debug(e1.getMessage(), e1);
          }
        }
      }

      /*
       * Shows history window.
       * 
       * When showed all actions are notified there. This may slow down the program
       */
      if (b == menuShowHistory) {
        JOptionPane.showMessageDialog(window,
                "The full history of changes is avaiable after saving your work in the" + " file "
                        + FileExtensions.newConfigFileExt);
        /*
         * if (historyLogger.isOpened()) historyLogger.closeHistory(); else
         * historyLogger.openHistory();
         */
      }

      /**
       * Load global config - QCONF file or paQP file. It depends on QuimP.newFileFormat
       * 
       * Checks also whether the name of the image sealed in config file is the same as those
       * opened currently. If not user has an option to break the procedure or continue
       * loading.
       */
      if (b == menuLoad || b == bnLoad) {
        FileDialogEx od = new FileDialogEx(IJ.getInstance());
        od.setDirectory(OpenDialog.getLastDirectory());
        try {
          if (QuimP.newFileFormat.get() == true) { // load QCONF
            od.setExtension(FileExtensions.newConfigFileExt);
            if (od.showOpenDialog() == null) {
              return;
            }
            loadQconfConfiguration(Paths.get(od.getDirectory(), od.getFile()));
          }
          if (QuimP.newFileFormat.get() == false) { // old paQP and snQP
            od.setExtension(FileExtensions.configFileExt);
            if (od.showOpenDialog() == null) {
              return;
            }
            if (qState.readParams(od.getPath().toFile())) {
              updateSpinnerValues();
              if (loadSnakes()) {
                run = false;
              } else {
                run = true;
              }
            }
          }
        } catch (IOException e1) {
          IJ.error("Problem with loading plugin config", e1.getMessage());
          LOGGER.debug(e1.getMessage(), e1); // if debug enabled - get more info
        } catch (JsonSyntaxException e1) {
          IJ.error("Problem with configuration file", e1.getMessage());
          LOGGER.debug(e1.getMessage(), e1);
        } catch (Exception e1) { // eg json but wrong
          IJ.error("Error", "File can not be loaded or parsed" + e1.getMessage());
          LOGGER.debug(e1.getMessage(), e1);
        }
      }

      /*
       * Discard all plugins.
       * 
       * In general it does: reset current snakePluginList and snakePluginListSnapshots,
       * Copies segSnakes to finalSnakes
       */
      if (b == menuDeletePlugin) {
        // clear all plugins
        qState.snakePluginList.clear();
        for (SnakePluginList sp : qState.snakePluginListSnapshots) {
          if (sp != null) {
            sp.clear();
          }
        }
        // copy snakes to finals
        for (int i = 0; i < qState.nest.size(); i++) {
          SnakeHandler snakeHandler = qState.nest.getHandler(i);
          snakeHandler.copyFromSegToFinal();
        }
        // update window
        imageGroup.updateOverlay(qState.boap.frame);
      }

      /*
       * Reload and all plugins stored in snakePluginListSnapshot. Note that plugins are not
       * executed, only loaded.
       * 
       * qState.snakePluginList.clear(); can not be called here because
       * com.github.celldynamics.quimp.BOAState.restore(int) makes reference to
       * snakePluginListSnapshot in snakePluginList. Thus, cleaning snakePluginList deletes
       * one entry in snakePluginListSnapshot
       */
      if (b == menuApplyPlugin) {
        // iterate over snapshots and try to restore plugins in snapshots
        for (SnakePluginList sp : qState.snakePluginListSnapshots) {
          sp.afterSerialize();
        }
        // copy snapshots for frame to current snakePluginList (and segParams)
        qState.restore(qState.boap.frame);
        // recalculatePlugins(); // update screen
      }

      /*
       * Copy current plugin tree to all frames and applies plugins.
       */
      if (b == menuPopulatePlugin) { // run in thread
        List<Integer> range = IntStream.rangeClosed(1, qState.boap.getFrames()).boxed()
                .collect(Collectors.toList());
        populatePlugins(range);
      }

      /*
       * Run segmentation from mask file.
       */
      if (b == menuSegmentationRun) {
        if (qState.binarySegmentationPlugin != null) {
          if (!qState.binarySegmentationPlugin.isWindowVisible()) {
            qState.binarySegmentationPlugin.showUi(true);
          }
        } else {
          qState.binarySegmentationPlugin = new BinarySegmentation_(); // create instance
          qState.binarySegmentationPlugin.attachNest(qState.nest); // attach data
          // allow plugin to update screen
          qState.binarySegmentationPlugin.attachContext(viewUpdater);
          // plugin is run internally after Apply update screen is always on Apply button of plugin
          qState.binarySegmentationPlugin.showUi(true);
        }
        qState.binarySegmentationPlugin.attachImagePlus(imageGroup.getOrgIpl());
        // regenerate stored data and create snapshot structures
        for (int f = 1; f <= qState.boap.getFrames(); f++) {
          qState.store(f);
        }
        BOA_.log("Run segmentation from mask file");
      }

      /*
       * Clean all bOA state.
       */
      if (b == menuSegmentationReset) {
        qState.reset(WindowManager.getCurrentImage(), pluginFactory, viewUpdater);
        qState.nest.cleanNest();
        imageGroup.clearPaths(1);
        setDefualts();
        if (qState.boap.frame != imageGroup.getOrgIpl().getSlice()) {
          imageGroup.updateToFrame(qState.boap.frame); // move to frame
        } else {
          updateSliceSelector(); // repaint window explicitly
        }
      }

      updateWindowState(); // window logic on any change and selectors

      // run segmentation for selected cases
      if (run) { // in thread
        runBoaThread(qState.boap.frame, qState.boap.frame, false);
      }
    }

    /**
     * If Freeze cell button is clicked.
     * 
     * @param status on or off this function
     */
    private void setFreeze(boolean status) {
      if (status) {
        bnFreezeCell.setLabel("*CANCEL*");
        qState.boap.doFreeze = true;
        lastTool = IJ.getToolName();
        IJ.setTool(Toolbar.LINE);
      } else {
        bnFreezeCell.setLabel("Freeze");
        qState.boap.doFreeze = false;
        IJ.setTool(lastTool);
      }
    }

    /**
     * Run segmentation in separate thread.
     * 
     * @param startFrame start frame
     * @param endFrame end frame
     * 
     * @param interruptible if true cancel button is active.
     */
    private void runBoaThread(int startFrame, int endFrame, boolean interruptible) {
      // run on current frame
      sww = new SwingWorker<Boolean, Object>() {
        @Override
        protected Boolean doInBackground() throws Exception {
          setBusyStatus(true, interruptible);
          IJ.showStatus("SEGMENTING...");
          runBoa(startFrame, endFrame);
          return true;
        }

        @Override
        protected void done() {
          try {
            get();
          } catch (ExecutionException e) {
            Throwable cause = e.getCause();
            if (cause instanceof BoaException) {
              ((BoaException) cause).setMessageSinkType(MessageSinkTypes.NONE);
              int framesCompleted = ((BoaException) cause).getFrame();
              String ret = ((BoaException) cause).handleException(IJ.getInstance(),
                      "FAIL AT " + framesCompleted);
              IJ.showStatus("FAIL AT " + framesCompleted);
              BOA_.log(ret);
            }
          } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
          } finally {
            setBusyStatus(false, true);
            IJ.showStatus("COMPLETE");
          }
        }

      };
      sww.execute();
    }

    /**
     * Copy plugin tree from current frame (current state of qState.snakePluginList) to other
     * frames.
     * 
     * @param frames List of frames to copy plugin stack to. Numbered from 1.
     * 
     * @see #actionPerformed(ActionEvent)
     */
    private void populatePlugins(List<Integer> frames) {
      if (frames.isEmpty()) {
        return;
      }
      SnakePluginList tmp = qState.snakePluginList.getDeepCopy();
      int cf = qState.boap.frame;

      sww = new SwingWorker<Boolean, Object>() {
        @Override
        protected Boolean doInBackground() throws Exception {
          setBusyStatus(true, true);
          IJ.showStatus("SEGMENTING...");
          IJ.showProgress(0, frames.get(frames.size() - 1) - frames.get(0));
          // iterate over frames and applies plugins
          for (int f : frames) {
            // make a deep copy
            qState.snakePluginListSnapshots.set(f - 1, tmp.getDeepCopy());
            qState.snakePluginListSnapshots.set(f - 1, tmp.getDeepCopy());
            // instance separate copy of jar for this plugin (in fact PluginFactory will return here
            // reference if this jar is already opened)
            qState.snakePluginListSnapshots.get(f - 1).afterSerialize();

            qState.boap.frame = f; // assign to global frame variable
            imageGroup.updateToFrame(qState.boap.frame);
            recalculatePlugins();
            if (isSegBreakHit == true) { // if flag set, stop
              isSegBreakHit = false;
              break;
            }
            IJ.showProgress(f, frames.get(frames.size() - 1));
          }
          qState.boap.frame = cf;
          imageGroup.updateToFrame(qState.boap.frame);
          return true;
        }

        @Override
        protected void done() {
          setBusyStatus(false, true);
          IJ.showStatus("COMPLETE");
          IJ.showProgress(2.0); // >1 to erase progress bar
        }

      };
      sww.execute();

    }

    /**
     * Loader of QCONF file in BOA. Initialise all BOA structures and updates window.
     * 
     * <p>Assign also format converter. This method partially updates UI but some other related
     * methods are called from {@link #actionPerformed(ActionEvent)}.
     * 
     * @param configPath path to QCONF file
     * @throws IOException on file problem
     * @throws Exception various other problems like e.g json syntax
     * @see #updateWindowState()
     */
    private void loadQconfConfiguration(Path configPath) throws IOException, Exception {
      Path filename = configPath.getFileName();
      if (filename == null) {
        throw new IllegalAccessException("Input path is not file");
      }
      Serializer<DataContainer> loaded; // loaded instance
      // create serializer
      Serializer<DataContainer> s = new Serializer<>(DataContainer.class, QuimP.TOOL_VERSION);
      s.registerConverter(new Converter170202<>(QuimP.TOOL_VERSION));
      s.registerInstanceCreator(DataContainer.class,
              new DataContainerInstanceCreator(pluginFactory, viewUpdater));
      loaded = s.load(configPath.toString());
      // check against image names
      if (!loaded.obj.BOAState.boap.getOrgFile().getName()
              .equals(qState.boap.getOrgFile().getName())) {
        LOGGER.warn("The image opened currently in BOA is different from those"
                + " pointed in configuration file");
        log("Trying to apply configuration saved for other image");
        YesNoCancelDialog yncd = new YesNoCancelDialog(IJ.getInstance(), "Warning",
                "Trying to load configuration that does not\nmath to"
                        + " opened image.\nAre you sure?");
        if (!yncd.yesPressed()) {
          return;
        }
      }
      // replace orgFile with that already opened. It is possible as BOA can not
      // exist without image loaded so this field will always be true.
      loaded.obj.BOAState.boap.setOrgFile(qState.boap.getOrgFile());
      // replace outputFileCore with current one
      String parent;
      if (configPath.getParent() != null) {
        parent = configPath.getParent().toString();
      } else {
        parent = "";
      }
      loaded.obj.BOAState.boap.setOutputFileCore(parent + File.separator + filename.toString());
      // closes windows, etc
      qState.reset(WindowManager.getCurrentImage(), pluginFactory, viewUpdater);
      qState = loaded.obj.BOAState;
      imageGroup.updateNest(qState.nest); // reconnect nest to external class
      qState.restore(qState.boap.frame); // copy from snapshots to current object
      updateSpinnerValues(); // update segmentation gui
      // refill frame zoom choice to make possible selection last zoomed cell (called from
      // updateChoice)
      fillZoomChoice();
      // do not recalculatePlugins here because pluginList is empty and this
      // method will update finalSnake overriding it by segSnake (because on
      // empty list they are just copied)
      // updateToFrame calls updateSliceSelector only if there is action of
      // changing frame. If loaded frame is the same as current one this event is
      // not called.
      updateMenus();
      if (qState.boap.frame != imageGroup.getOrgIpl().getSlice()) {
        // move to frame (will call updateSliceSelector)
        imageGroup.updateToFrame(qState.boap.frame);
      } else {
        updateSliceSelector(); // repaint window explicitly
      }
      BOA_.log("Configuration read successfully");
    }

    /**
     * Detect changes in checkboxes and run segmentation for current frame if necessary.
     * 
     * <p>Transfer parameters from changed GUI element to
     * {@link com.github.celldynamics.quimp.BOAState.BOAp} class
     * 
     * @param e Type of event
     * @see com.github.celldynamics.quimp.BOA_.CustomStackWindow#updateWindowState()
     */
    @Override
    public void itemStateChanged(final ItemEvent e) {
      LOGGER.trace("EVENT:itemStateChanged");
      setFreeze(false); // disable if active
      if (qState.boap.doDelete) {
        BOA_.log("**WARNING:DELETE IS ON**");
      }
      boolean run = false; // set to true if any of items changes require to re-run segmentation
      Object source = e.getItemSelectable();
      if (source == cbPath) {
        qState.segParam.showPaths = cbPath.getState();
        if (qState.segParam.showPaths) {
          this.setImage(imageGroup.getPathsIpl());
        } else {
          this.setImage(imageGroup.getOrgIpl());
        }
        if (qState.boap.zoom && !qState.nest.isVacant()) { // set zoom
          imageGroup.zoom(canvas, qState.boap.frame, qState.boap.snakeToZoom);
        }
      } else if (source == cbPrevSnake) {
        qState.segParam.use_previous_snake = cbPrevSnake.getState();
      } else if (source == cbExpSnake) {
        qState.segParam.expandSnake = cbExpSnake.getState();
        run = true;
      } else if (source == cbContractingDirection) {
        qState.segParam.contractingDirection = cbContractingDirection.getState();
        qState.segParam.reverseForces();
        updateSpinnerValues();
        run = true;
      } else if (source == cbFirstPluginActiv) { // run in thread
        qState.snakePluginList.setActive(0, cbFirstPluginActiv.getState());
        setBusyStatus(true, false);
        recalculatePlugins();
      } else if (source == cbSecondPluginActiv) { // run in thread
        qState.snakePluginList.setActive(1, cbSecondPluginActiv.getState());
        setBusyStatus(true, false);
        recalculatePlugins();
      } else if (source == cbThirdPluginActiv) { // run in thread
        qState.snakePluginList.setActive(2, cbThirdPluginActiv.getState());
        setBusyStatus(true, false);
        recalculatePlugins();
      }

      // action on menus
      if (source == cbMenuPlotOriginalSnakes) { // run in thread
        qState.boap.isProcessedSnakePlotted = cbMenuPlotOriginalSnakes.getState();
        setBusyStatus(true, false);
        recalculatePlugins();
      }
      if (source == cbMenuPlotHead) {
        qState.boap.isHeadPlotted = cbMenuPlotHead.getState();
        imageGroup.updateOverlay(qState.boap.frame);
      }
      if (source == cbMenuZoomFreeze) {
        qState.boap.isZoomFreeze = cbMenuZoomFreeze.getState();
        if (qState.boap.isZoomFreeze && qState.boap.zoom) { // set in zoom mode
          JOptionPane.showMessageDialog(window, QuimpToolsCollection
                  .stringWrap("Please un-zoom your view first.", QuimP.LINE_WRAP), "Error",
                  JOptionPane.ERROR_MESSAGE);
          qState.boap.isZoomFreeze = false;
          cbMenuZoomFreeze.setState(false); // unselect
        }
      }
      // actions on Plugin selections
      if (source == chFirstPluginName) { // run in thread
        LOGGER.debug("Used firstPluginName, val: " + chFirstPluginName.getSelectedItem());
        instanceSnakePlugin((String) chFirstPluginName.getSelectedItem(), 0,
                cbFirstPluginActiv.getState());
        setBusyStatus(true, false);
        recalculatePlugins();
      }
      if (source == chSecondPluginName) { // run in thread
        LOGGER.debug("Used secondPluginName, val: " + chSecondPluginName.getSelectedItem());
        instanceSnakePlugin((String) chSecondPluginName.getSelectedItem(), 1,
                cbSecondPluginActiv.getState());
        setBusyStatus(true, false);
        recalculatePlugins();
      }
      if (source == chThirdPluginName) { // run in thread
        LOGGER.debug("Used thirdPluginName, val: " + chThirdPluginName.getSelectedItem());
        instanceSnakePlugin((String) chThirdPluginName.getSelectedItem(), 2,
                cbThirdPluginActiv.getState());
        setBusyStatus(true, false);
        recalculatePlugins();
      }

      // Action on zoom selector
      if (source == chZoom) {
        LOGGER.trace("zoom val " + chZoom.getSelectedItem());
        if (chZoom.getSelectedItem().equals(fullZoom)) { // user selected default position (no zoom)
          qState.boap.snakeToZoom = -1; // set negative value to indicate no zoom
          qState.boap.zoom = false; // important for other parts (legacy)
          imageGroup.unzoom(canvas); // unzoom view
          // unfreeze all
          if (qState.boap.isZoomFreeze == true) {
            for (SnakeHandler sh : qState.nest.getHandlers()) {
              sh.unfreezeHandler();
            }
          }
        } else { // zoom here
          if (!qState.nest.isVacant()) { // any snakes present
            qState.boap.snakeToZoom = Integer.parseInt(chZoom.getSelectedItem()); // get int
            qState.boap.zoom = true; // legacy compatibility
            // snakeID, not index
            SnakeHandler snakeH = qState.nest.getHandlerofId(qState.boap.snakeToZoom);
            if (qState.boap.isZoomFreeze == true && snakeH != null
                    && snakeH.isStoredAt(qState.boap.frame)) {
              for (SnakeHandler sh : qState.nest.getHandlers()) {
                sh.freezeHandler();
              } // freeze all except:
              snakeH.unfreezeHandler();
            }
            imageGroup.zoom(canvas, qState.boap.frame, qState.boap.snakeToZoom);
          }
        }
        imageGroup.updateOverlay(qState.boap.frame); // to have proper colors for frozen snakes
      }

      updateWindowState(); // window logic on any change
      updateChoices(); // only for updating colors after updating window state

      try {
        if (run) {
          if (supressStateChangeBOArun) { // when spinners are changed
            // programmatically they raise the event. this is to block segmentation re-run
            LOGGER.debug("supressState");
            return;
          }
          // run on current frame
          runBoa(qState.boap.frame, qState.boap.frame);
        }
      } catch (BoaException be) {
        be.setMessageSinkType(MessageSinkTypes.NONE);
        BOA_.log(be.handleException(IJ.getInstance(), "Segmentation failed at " + be.getFrame()));
      } finally {
        setBusyStatus(false, true);
      }
    }

    /**
     * Detect changes in spinners and run segmentation for current frame if necessary.
     * 
     * <p>Transfer parameters from changed GUI element to
     * {@link com.github.celldynamics.quimp.BOAState.BOAp} class
     * 
     * @param ce Type of event
     * @see com.github.celldynamics.quimp.BOA_.CustomStackWindow#updateWindowState()
     */
    @Override
    public void stateChanged(final ChangeEvent ce) {
      LOGGER.trace("EVENT:stateChanged");
      setFreeze(false); // disable if active
      if (qState.boap.doDelete) {
        BOA_.log("**WARNING:DELETE IS ON**");
      }
      boolean run = false; // set to true if any of items changes require to re-run segmentation
      Object source = ce.getSource();

      if (source == dsNodeRes) {
        JSpinner spinner = (JSpinner) source;
        qState.segParam.setNodeRes((Double) spinner.getValue());
        run = true;
      } else if (source == dsVelCrit) {
        JSpinner spinner = (JSpinner) source;
        qState.segParam.vel_crit = (Double) spinner.getValue();
        run = true;
      } else if (source == dsFImage) {
        JSpinner spinner = (JSpinner) source;
        qState.segParam.f_image = (Double) spinner.getValue();
        run = true;
      } else if (source == dsFCentral) {
        JSpinner spinner = (JSpinner) source;
        qState.segParam.f_central = (Double) spinner.getValue();
        run = true;
      } else if (source == dsFContract) {
        JSpinner spinner = (JSpinner) source;
        qState.segParam.f_contract = (Double) spinner.getValue();
        run = true;
      } else if (source == dsFinalShrink) {
        JSpinner spinner = (JSpinner) source;
        qState.segParam.finalShrink = (Double) spinner.getValue();
        run = true;
      } else if (source == isMaxIterations) {
        JSpinner spinner = (JSpinner) source;
        qState.segParam.max_iterations = (Integer) spinner.getValue();
        run = true;
      } else if (source == isBlowup) {
        JSpinner spinner = (JSpinner) source;
        qState.segParam.blowup = (Integer) spinner.getValue();
        run = true;
      } else if (source == isSampletan) {
        JSpinner spinner = (JSpinner) source;
        qState.segParam.sample_tan = (Integer) spinner.getValue();
        run = true;
      } else if (source == isSamplenorm) {
        JSpinner spinner = (JSpinner) source;
        qState.segParam.sample_norm = (Integer) spinner.getValue();
        run = true;
      }

      updateWindowState(); // window logic on any change

      try {
        if (run) {
          // when spinners are changed programmatically they raise the event. this is to block
          // segmentation re-run
          if (supressStateChangeBOArun) {
            LOGGER.debug("supressState");
            return;
          }
          // run on current frame
          runBoa(qState.boap.frame, qState.boap.frame);
        }
      } catch (BoaException be) {
        be.setMessageSinkType(MessageSinkTypes.NONE);
        BOA_.log(be.handleException(IJ.getInstance(), "Segmentation failed " + be.getFrame()));
      }
    }

    /**
     * Update the frame label, overlay, frame and set zoom Called when user clicks on slice
     * selector in IJ window.
     */
    @Override
    public void updateSliceSelector() {
      super.updateSliceSelector();
      LOGGER.trace("EVENT:updateSliceSelector");
      if (!qState.boap.singleImage) {
        zSelector.setValue(imp.getCurrentSlice()); // this is delayed in
        // super.updateSliceSelector force it now
      }
      if (qState.boap.editMode) {
        // BOA_.log("next frame in edit mode");
        stopEdit();
      }

      if (!qState.boap.singleImage) {
        qState.boap.frame = imp.getCurrentSlice();
        frameLabel.setText("" + qState.boap.frame);
      }
      imageGroup.updateOverlay(qState.boap.frame); // draw overlay
      imageGroup.setIpSliceAll(qState.boap.frame);

      // zoom to snake zero
      if (qState.boap.zoom && !qState.nest.isVacant()) {
        imageGroup.zoom(canvas, qState.boap.frame, qState.boap.snakeToZoom);
      }
      // if in edit, save current edit and start edit of next frame if exists
      boolean wasInEdit = qState.boap.editMode;
      if (wasInEdit) {
        bnEdit.setLabel("*STOP EDIT*");
        BOA_.log("**EDIT IS ON**");
        qState.boap.editMode = true;
        lastTool = IJ.getToolName();
        IJ.setTool(Toolbar.LINE);
        editSeg(0, 0, qState.boap.frame);
        IJ.setTool(lastTool);
      }
      LOGGER.trace("Snakes at this frame: " + qState.nest.getSnakesforFrame(qState.boap.frame));
      if (!isSegRunning) {
        // do not update or restore state when we hit this event from runBoa() method (through
        // setIpSliceAll(int))
        qState.restore(qState.boap.frame);
        updateSpinnerValues();
        updateWindowState();
      }
    }

    /**
     * Turn delete mode off by setting proper value in
     * {@link com.github.celldynamics.quimp.BOAState.BOAp}.
     */
    void switchOffDelete() {
      qState.boap.doDelete = false;
      bnDel.setLabel("Delete cell");
    }

    /*
     * (non-Javadoc)
     * 
     * @see ij.gui.ImageWindow#setImage(ij.ImagePlus)
     */
    @Override
    public void setImage(ImagePlus imp2) {
      double m = this.ic.getMagnification();
      Dimension dem = this.ic.getSize();
      super.setImage(imp2);
      this.ic.setMagnification(m);
      this.ic.setSize(dem);
    }

    /**
     * Turn truncate mode off by setting proper value in
     * {@link com.github.celldynamics.quimp.BOAState.BOAp}.
     */
    void switchOfftruncate() {
      qState.boap.doDeleteSeg = false;
      bnDelSeg.setLabel("Truncate Seg");
    }

    /**
     * Set frame interval and scale on BOA window..
     */
    void setScalesText() {
      pixelLabel.setText("Scale: " + IJ.d2s(qState.boap.getImageScale(), 6) + " \u00B5m");
      fpsLabel.setText("F Interval: " + IJ.d2s(qState.boap.getImageFrameInterval(), 3) + " s");
    }

  } // end of CustomStackWindow

  /**
   * Creates instance (through SnakePluginList) of plugin of given name on given UI slot.
   * 
   * <p>Decides if plugin will be created or destroyed basing on plugin name from Choice list
   * 
   * @param selectedPlugin Name of plugin returned from UI elements
   * @param slot Slot of plugin
   * @param act Indicates if plugins is activated in GUI
   * @see com.github.celldynamics.quimp.SnakePluginList
   */
  private void instanceSnakePlugin(final String selectedPlugin, int slot, boolean act) {

    try {
      // get instance using plugin name (obtained from getPluginNames from PluginFactory
      if (selectedPlugin != NONE) { // do no pass NONE to pluginFact
        qState.snakePluginList.setInstance(slot, selectedPlugin, act); // build instance
      } else {
        if (qState.snakePluginList.getInstance(slot) != null) {
          qState.snakePluginList.getInstance(slot).showUi(false);
        }
        qState.snakePluginList.deletePlugin(slot);
      }
    } catch (QuimpPluginException e) {
      IJ.error("Plugin " + selectedPlugin + " cannot be loaded.", "Reason: " + e.getMessage());
      LOGGER.debug(e.getMessage(), e);
    }
  }

  /**
   * Set busy status for BOA window.
   * 
   * <p>Window is inactive for busy status. Setting flag <tt>interruptible</tt> enables Cancel
   * button that breaks current operation.
   * 
   * @param busy True if busy, false otherwise
   * @param interruptible true for enabling Cancel button. Ignored if busy==false
   */
  private void setBusyStatus(boolean busy, boolean interruptible) {
    if (busy == true) {
      window.bnSeg.setBackground(Color.RED);
      window.bnSeg.setLabel("Busy");
      if (interruptible) {
        window.enableUiInterruptile(false);
      } else {
        window.enableUi(false);
      }
    } else {
      window.bnSeg.setBackground(null);
      window.bnSeg.setLabel("SEGMENT");
      window.enableUi(true);
    }

  }

  /**
   * Start segmentation process on range of frames.
   * 
   * <p>This method is called for update only current view as well (<tt>startF</tt> ==
   * <tt>endF</tt>). It also go through plugin stack.
   * 
   * @param startF start frame
   * @param endF end frame
   * @throws BoaException on any error
   */
  public void runBoa(int startF, int endF) throws BoaException {
    LOGGER.debug("run BOA");
    isSegBreakHit = false;
    isSegRunning = true;
    if (qState.nest.isVacant() || qState.nest.allFrozen()) {
      BOA_.log("Nothing to segment!");
      isSegRunning = false;
      return;
    }
    try {
      IJ.showProgress(0, endF - startF);

      qState.nest.resetForFrame(startF);
      if (!qState.segParam.expandSnake) {
        // blowup snake ready for contraction (only those not starting at or after the startF)
        constrictor.loosen(qState.nest, startF);
      } else {
        constrictor.implode(qState.nest, startF);
      }
      SnakeHandler snH;
      int s = 0;
      Snake snake;
      imageGroup.clearPaths(startF);
      LOGGER.debug("Use options: " + qState.segParam.toString());
      for (qState.boap.frame = startF; qState.boap.frame <= endF; qState.boap.frame++) {
        if (isSegBreakHit == true) {
          qState.boap.frame--;
          isSegBreakHit = false;
          break;
        }
        // per frame
        imageGroup.setProcessor(qState.boap.frame);
        imageGroup.setIpSliceAll(qState.boap.frame);

        try {
          if (qState.boap.frame != startF) { // expand snakes for next frame
            if (!qState.segParam.use_previous_snake) {
              qState.nest.resetForFrame(qState.boap.frame); // #274 block here as well? liveSnake
            } else {
              if (!qState.segParam.expandSnake) {
                constrictor.loosen(qState.nest, qState.boap.frame); // #274 here? this is about LS
              } else {
                constrictor.implode(qState.nest, qState.boap.frame); // #274 and here
              }
            }
          }

          for (s = 0; s < qState.nest.size(); s++) { // for each snake
            snH = qState.nest.getHandler(s);
            snake = snH.getLiveSnake();
            try {
              if (!snake.alive || qState.boap.frame < snH.getStartFrame()) {
                continue;
              }
              // process all snakes even if frozen to control overlaps (computed for liveSnakes) but
              // do not store any live snake from frozen snake
              if (!snH.isSnakeHandlerFrozen()) {
                imageGroup.drawPath(snake, qState.boap.frame); // pre tightned snake on path
                tightenSnake(snake);
                imageGroup.drawPath(snake, qState.boap.frame); // post tightned snake on path
                snH.backupLiveSnake(qState.boap.frame);
                Snake out = iterateOverSnakePlugins(snake);
                snH.storeThisSnake(out, qState.boap.frame); // store resulting snake as final
              } else {
                // overlaps are tested for liveSnakes (loosen) so update liveSnake to result of
                // segmentation for frozen snakehandler
                snH.copyFromFinalToLive(qState.boap.frame);
                LOGGER.debug("SnakeHandler " + snH.getID() + " is frozen");
              }
            } catch (QuimpPluginException qpe) {
              // must be rewritten with whole runBOA #65 #67
              qpe.setMessageSinkType(MessageSinkTypes.NONE);
              BOA_.log(qpe.handleException(null, "Error in filter module"));
              snH.storeLiveSnake(qState.boap.frame); // store segmented nonmodified
            } catch (BoaException be) { // from tighten
              imageGroup.drawPath(snake, qState.boap.frame); // failed position
              snH.storeLiveSnake(qState.boap.frame);
              snH.backupLiveSnake(qState.boap.frame);
              qState.nest.kill(snH);
              snake.unfreezeAll();
              be.setMessageSinkType(MessageSinkTypes.NONE);
              BOA_.log(be.handleException(null,
                      "Snake " + snake.getSnakeID() + " died, frame " + qState.boap.frame));
              isSegRunning = false;
              if (qState.nest.allDead()) { // end of processing (see condition in catch)
                throw new BoaException("All snakes dead: " + be.getMessage(), qState.boap.frame, 1);
              }
            }
          }
          // imageGroup.updateOverlay(qState.boap.frame); // redraw display
          IJ.showProgress(qState.boap.frame, endF);
        } catch (BoaException be) {
          isSegRunning = false;
          if (!qState.segParam.use_previous_snake) {
            imageGroup.setIpSliceAll(qState.boap.frame);
            imageGroup.updateOverlay(qState.boap.frame);
          } else { // end iterating if all snakes dead and we use previous
            throw be; // do no add LOGGER here #278
          }
        } finally {
          // historyLogger.addEntry("Processing", qState);
          qState.store(qState.boap.frame); // always remember state of the BOA that is
          // used for segmentation
        }
      }
      qState.boap.frame = endF;
    } catch (BoaException be) { // these from unexpected stopping of alg
      if (be.getFrame() == 0) { // if not set by thrower
        be.setFrame(qState.boap.frame);
      }
      throw be; // just rethrow them
    } catch (Exception e) { // any other (should not happen)
      // do no add LOGGER here #278
      BoaException be = new BoaException(e);
      be.setFrame(qState.boap.frame);
      throw be;
    } finally {
      isSegRunning = false;
      imageGroup.updateOverlay(qState.boap.frame); // update on error
      IJ.showProgress(2.0); // >1 to erase progress bar
    }

  }

  /**
   * Perform AC segmentation. Tighten snake around object.
   * 
   * <p>This method starts with snakes ({@link SnakeHandler#getLiveSnake()} that were blown up by
   * {@link Constrictor#loosen(Nest, int)} counteracting overlaps. If
   * {@link BOAState.SegParam#use_previous_snake} was not set, initial snake is produced from
   * original ROI by {@link Nest#resetForFrame(int)}. Then
   * {@link Constrictor#constrict(Snake, ImageProcessor)} is called many times, each time liveSnake
   * is moved slightly. Note that <b>liveSnake</b> is the same for each frame for given
   * {@link SnakeHandler}, this is why it can be used for seeding next frame.
   * 
   * @param snake snake to process
   * @throws BoaException if there is too less nodes left
   * @see SegParam#max_iterations
   */
  private void tightenSnake(final Snake snake) throws BoaException {

    int i;

    for (i = 0; i < qState.segParam.max_iterations; i++) { // iter constrict snake
      // if snakes are expanded from cell inside, testing against overlapping shuld be done in
      // expanding step but not in prparatory (loosen) as for constricting
      // if (qState.segParam.contractingDirection == false) { // expand from inside
      // constrictor.freezeProxSnakes(qState.nest, qState.boap.frame);
      // }
      if (i % qState.boap.cut_every == 0) {
        snake.cutLoops(); // cut out loops every p.cut_every timesteps
      }
      if (i % 10 == 0 && i != 0) {
        snake.correctDistance(true);
      }
      if (constrictor.constrict(snake, imageGroup.getOrgIp())) { // if all nodes frozen
        break;
      }
      if (i % 4 == 0) {
        imageGroup.drawPath(snake, qState.boap.frame); // draw current snake
      }

      if ((snake.getNumPoints() / snake.startingNnodes) > qState.boap.NMAX) {
        // if max nodes reached (as % starting) prompt for reset
        if (qState.segParam.use_previous_snake) {
          // imageGroup.drawContour(snake, frame);
          // imageGroup.updateAndDraw();
          throw new BoaException(
                  "Frame " + qState.boap.frame + "-max nodes reached " + snake.getNumPoints(),
                  qState.boap.frame, 1);
        } else {
          BOA_.log("Frame " + qState.boap.frame + "-max nodes reached..continue");
          break;
        }
      }
    }
    snake.unfreezeAll(); // set freeze tag back to false

    if (!qState.segParam.expandSnake) { // shrink a bit to get final outline
      if (BOA_.qState.segParam.contractingDirection) { // standard behaviour
        snake.scaleSnake(-BOA_.qState.segParam.finalShrink, 0.5, false);
      } else { // expanding from cell inside, set the same as contracting
        snake.scaleSnake(-BOA_.qState.segParam.finalShrink, 0.5, false);
      }
    }
    snake.cutLoops();
    snake.cutIntersects();
  }

  /**
   * Process Snake by all active plugins.
   * 
   * <p>Processed Snake is returned as new Snake with the same ID. Input snake is not modified. For
   * empty plugin list it just return input snake
   *
   * <p>This method supports two interfaces:
   * {@link com.github.celldynamics.quimp.plugin.snakes.IQuimpBOAPoint2dFilter},
   * {@link com.github.celldynamics.quimp.plugin.snakes.IQuimpBOASnakeFilter}
   * 
   * <p>It uses smart method to detect which interface is used for every slot to avoid unnecessary
   * conversion between data. <tt>previousConversion</tt> keeps what interface was used on
   * previous slot in plugin stack. Then for every plugin data are converted if current plugin
   * differs from previous one. Converted data are kept in <tt>snakeToProcess</tt> and
   * <tt>dataToProcess</tt> but only one of these variables is valid in given time. Finally after
   * last plugin data are converted to Snake.
   * 
   * @param snake snake to be processed
   * @return Processed snake or original input one when there is no plugin selected
   * @throws QuimpPluginException on plugin error
   * @throws BoaException on defective snake
   */
  private Snake iterateOverSnakePlugins(final Snake snake)
          throws QuimpPluginException, BoaException {
    final int ipoint = 0; // define IQuimpPoint2dFilter interface
    final int isnake = 1; // define IQuimpPoint2dFilter interface
    // type of previous plugin. Define if data should be converted for current plugin
    int previousConversion = isnake; // IQuimpSnakeFilter is default interface
    Snake outsnake = snake; // if there is no plugin just return input snake
    Snake snakeToProcess = snake; // data to be processed, input snake on beginning
    // data to process in format of list
    // null but it will be overwritten in loop because first "if" fires always (previousConversion
    // is set to isnake) on beginning, if first plugin is ipoint type
    List<Point2d> dataToProcess = null;
    if (!qState.snakePluginList.isRefListEmpty()) {
      LOGGER.debug("sPluginList not empty");
      for (Plugin qsP : qState.snakePluginList.getList()) { // iterate over list
        if (!qsP.isExecutable()) {
          continue; // no plugin on this slot or not active
        }
        if (qsP.getRef() instanceof IQuimpPluginAttachImage) {
          ((IQuimpPluginAttachImage) qsP.getRef()).attachImage(imageGroup.getOrgIp());
        }
        if (qsP.getRef() instanceof IQuimpBOAPoint2dFilter) { // check interface type
          if (previousConversion == isnake) { // previous was IQuimpSnakeFilter
            dataToProcess = snakeToProcess.asList(); // and data needs to be converted
          }
          IQuimpBOAPoint2dFilter qsPcast = (IQuimpBOAPoint2dFilter) qsP.getRef();
          qsPcast.attachData(dataToProcess);
          dataToProcess = qsPcast.runPlugin(); // store result in input variable
          previousConversion = ipoint;
        }
        if (qsP.getRef() instanceof IQuimpBOASnakeFilter) { // check interface type
          if (previousConversion == ipoint) { // previous was IQuimpPoint2dFilter
            // and data must be converted to snake from dataToProcess
            snakeToProcess = new QuimpDataConverter(dataToProcess).getSnake(snake.getSnakeID());
          }
          IQuimpBOASnakeFilter qsPcast = (IQuimpBOASnakeFilter) qsP.getRef();
          qsPcast.attachData(snakeToProcess);
          snakeToProcess = qsPcast.runPlugin(); // store result as snake for next plugin
          previousConversion = isnake;
        }
      }
      // after loop previousConversion points what plugin was last and actual data
      // must be converted to snake
      switch (previousConversion) {
        case ipoint: // last plugin was IQuimpPoint2dFilter - convert to Snake
          outsnake = new QuimpDataConverter(dataToProcess).getSnake(snake.getSnakeID());
          break;
        case isnake: // last plugin was IQuimpSnakeFilter - do not convert
          outsnake = snakeToProcess;
          outsnake.setSnakeID(snake.getSnakeID()); // copy old id in case if user forgot
          break;
        default:
          throw new IllegalArgumentException("Unknown previousConversion");
      }
    } else {
      LOGGER.debug("sPluginList empty");
    }
    return outsnake;
  }

  /**
   * Sets the scales.
   * 
   * <p>Scale and interval fields are already initialised in {@link BOAState} constructor from
   * loaded image. If image does not have proper scale or interval, defaults from
   * {@link BOAState.BOAp#setImageScale(double)} and
   * {@link BOAState.BOAp#setImageFrameInterval(double)} are taken.
   * 
   * <p>All stats are evaluated using scales stored in tiff file so those values put here by user
   * are copied to image by {@link #updateImageScale()}.
   */
  void setScales() {
    GenericDialog gd = new GenericDialog("Set image scale", window);
    gd.addNumericField("Frame interval (seconds)", qState.boap.getImageFrameInterval(), 3);
    gd.addNumericField("Pixel width (\u00B5m)", qState.boap.getImageScale(), 6);
    gd.showDialog();

    double tempFI = gd.getNextNumber(); // force to check for errors
    double tempP = gd.getNextNumber();

    if (gd.invalidNumber()) {
      IJ.error("Values invalid");
      BOA_.log("Scale was not updated:\n\tinvalid input");
    } else if (gd.wasOKed()) {
      qState.boap.setImageFrameInterval(tempFI);
      qState.boap.setImageScale(tempP);
      updateImageScale();
      BOA_.log("Scale successfully updated");
    }

  }

  /**
   * Update image scale.
   */
  void updateImageScale() {
    imageGroup.getOrgIpl().getCalibration().frameInterval = qState.boap.getImageFrameInterval();
    imageGroup.getOrgIpl().getCalibration().pixelHeight = qState.boap.getImageScale();
    imageGroup.getOrgIpl().getCalibration().pixelWidth = qState.boap.getImageScale();
  }

  /**
   * Load snakes from snQP file.
   *
   * @return true, if successful
   */
  private boolean loadSnakes() {

    YesNoCancelDialog yncd = new YesNoCancelDialog(IJ.getInstance(), "Load associated snakes?",
            "\tLoad associated snakes?\n");
    if (!yncd.yesPressed()) {
      return false;
    }

    OutlineHandler otlineH = new OutlineHandler(qState.boap.readQp);
    if (!otlineH.readSuccess) {
      BOA_.log("Could not read in snakes");
      return false;
    }
    // convert to BOA snakes

    qState.nest.addOutlinehandler(otlineH);
    imageGroup.setProcessor(otlineH.getStartFrame());
    imageGroup.updateOverlay(otlineH.getStartFrame());
    BOA_.log("Successfully read snakes");
    return true;
  }

  /**
   * Add ROI to Nest.
   * 
   * <p>This method is called on selection that should contain object to be segmented. Initialise
   * Snake object in Nest and it performs also initial segmentation of selected cell.
   * 
   * @param r ROI object (IJ)
   * @param f number of current frame
   * @see #tightenSnake(Snake)
   */
  // @SuppressWarnings("unchecked")
  void addCell(final Roi r, int f) {
    SnakeHandler snakeH = qState.nest.addHandler(r, f);
    if (snakeH == null) { // cell failed to initialise
      return;
    }
    Snake snake = snakeH.getLiveSnake();
    imageGroup.setProcessor(f);
    try {
      LOGGER.debug("Use options: " + qState.segParam.toString());
      imageGroup.drawPath(snake, f); // pre tightned snake on path
      tightenSnake(snake);
      imageGroup.drawPath(snake, f); // post tightned snake on path
      snakeH.backupLiveSnake(f);
      Snake out = iterateOverSnakePlugins(snake); // process segmented snake by plugins
      snakeH.storeThisSnake(out, f); // store processed snake as final

      // if any problem with plugin or other, store snake without modification
      // because snake.asList() returns copy
    } catch (QuimpPluginException qpe) {
      qpe.setMessageSinkType(MessageSinkTypes.NONE);
      BOA_.log(qpe.handleException(null, "Error in filter module"));
      snakeH.storeLiveSnake(f);
    } catch (BoaException be) {
      snakeH.deleteStoreAt(f);
      snakeH.kill();
      snakeH.backupLiveSnake(f);
      snakeH.storeLiveSnake(f);
      be.setMessageSinkType(MessageSinkTypes.NONE);
      BOA_.log(be.handleException(null, "New snake failed to converge"));
    } catch (Exception e) {
      IJ.error("Error", e.getMessage());
      LOGGER.debug(e.getMessage(), e);
    } finally {
      imageGroup.updateOverlay(f);
      // historyLogger.addEntry("Added cell", qState);
      qState.store(f); // always remember state of the BOA after modification of UI
    }

  }

  /**
   * Delete SnakeHandler using the snake clicked by user.
   * 
   * <p>Method searches the snake in Nest that is on current frame and its centroid is close enough
   * to clicked point. If found, the whole SnakeHandler (all Snakes of the same ID across frames)
   * is deleted.
   * 
   * @param offScreenX clicked coordinate
   * @param offScreenY clicked coordinate
   * @param frame current frame
   * @return true if handler deleted, false if not (because user does not click it)
   * @see #freezeCell(int, int, int)
   */
  boolean deleteCell(int offScreenX, int offScreenY, int frame) {
    if (qState.nest.isVacant()) {
      return false;
    }

    SnakeHandler sh =
            qState.nest.findClosestTo(offScreenX, offScreenY, frame, QuimP.mouseSensitivity);
    if (sh != null) { // if closest < 10, delete it
      BOA_.log("Deleted cell " + sh.getID());
      qState.nest.removeHandler(sh);
      imageGroup.updateOverlay(frame);
      window.switchOffDelete();
      return true;
    } else {
      BOA_.log("Click the cell centre to delete");
    }
    return false;
  }

  /**
   * Freeze SnakeHandler closes to specified coordinates.
   * 
   * <p>Coordinates relate to Snake from SnakeHandler.
   * 
   * @param offScreenX clicked coordinate
   * @param offScreenY clicked coordinate
   * @param frame frame
   * @return true on success.
   * @see #deleteCell(int, int, int)
   */
  boolean freezeCell(int offScreenX, int offScreenY, int frame) {
    if (qState.nest.isVacant()) {
      return false;
    }

    SnakeHandler sh =
            qState.nest.findClosestTo(offScreenX, offScreenY, frame, QuimP.mouseSensitivity);
    if (sh != null) { // if closest < 10, delete it
      if (sh.isSnakeHandlerFrozen()) {
        sh.unfreezeHandler();
        BOA_.log("Unfreezed cell " + sh.getID());
      } else {
        sh.freezeHandler();
        BOA_.log("Freezed cell " + sh.getID());
      }
      imageGroup.updateOverlay(frame);
      window.setFreeze(false);
      return true;
    } else {
      BOA_.log("Click the cell centre to freeze");
    }
    return false;

  }

  /**
   * Delete segmentation.
   *
   * @param offScreenX Coordinate of clicked point
   * @param offScreenY Coordinate of clicked point
   * @param frame the frame
   */
  void deleteSegmentation(int offScreenX, int offScreenY, int frame) {

    SnakeHandler sh =
            qState.nest.findClosestTo(offScreenX, offScreenY, frame, QuimP.mouseSensitivity);
    if (sh != null) {
      BOA_.log("Deleted snake " + sh.getID() + " from " + frame + " onwards");
      sh.deleteStoreFrom(frame);
      imageGroup.updateOverlay(frame);
      window.switchOfftruncate();
    } else {
      BOA_.log("Click the cell centre to delete");
    }
  }

  /**
   * Called when user click Edit button.
   * 
   * @param offScreenX Coordinate of clicked point
   * @param offScreenY Coordinate of clicked point
   * @param frame current frame in stack
   * @see #stopEdit()
   * @see com.github.celldynamics.quimp.BOA_.CustomStackWindow#updateSliceSelector()
   */
  void editSeg(int offScreenX, int offScreenY, int frame) {

    SnakeHandler sh = null;
    if (qState.nest.size() == 1) {
      Optional<SnakeHandler> firstSh =
              qState.nest.getHandlers().stream().filter(p -> p != null).findFirst();
      sh = firstSh.orElse(null);
    } else {
      sh = qState.nest.findClosestTo(offScreenX, offScreenY, frame, QuimP.mouseSensitivity);
    }
    if (sh != null) {
      qState.boap.editingID = sh.getID(); // sH.getID();
      BOA_.log("Editing cell " + sh.getID());
      imageGroup.clearOverlay();

      Roi r;
      if (qState.boap.useSubPixel == true) {
        r = sh.getStoredSnake(frame).asPolyLine();
      } else {
        r = sh.getStoredSnake(frame).asIntRoi();
      }
      // Roi r = sH.getStoredSnake(frame).asFloatRoi();
      Roi.setColor(Color.cyan);
      canvas.getImage().setRoi(r);
    } else {
      BOA_.log("Click a cell centre to edit");
    }
  }

  /**
   * Called when user ends editing.
   * 
   * @see com.github.celldynamics.quimp.BOA_.CustomStackWindow#updateSliceSelector()
   */
  void stopEdit() {
    Roi r = canvas.getImage().getRoi();
    Roi.setColor(Color.yellow);
    SnakeHandler snakeH = qState.nest.getHandler(qState.boap.editingID);
    snakeH.storeRoi((PolygonRoi) r, qState.boap.frame); // store as final snake
    // copy to segSnakes array
    Snake stored = snakeH.getStoredSnake(qState.boap.frame);
    snakeH.backupThisSnake(stored, qState.boap.frame);
    // and run filters and store in final again
    try {
      Snake out = iterateOverSnakePlugins(stored); // process segmented snake by plugins
      snakeH.storeThisSnake(out, qState.boap.frame); // store processed snake as final
    } catch (QuimpException e) {
      e.setMessageSinkType(MessageSinkTypes.NONE);
      BOA_.log(e.handleException(null, "Error in filter module."));
      // TODO now stored in original snake (by storeRoi) but here can be decision what to do
    }
    canvas.getImage().killRoi();
    imageGroup.updateOverlay(qState.boap.frame);
    qState.boap.editingID = -1;
  }

  /**
   * Delete seg.
   *
   * @param x the x
   * @param y the y
   */
  void deleteSeg(int x, int y) {
  }

  /**
   * Initialising all data saving and exporting results to disk and IJ.
   * 
   * @param finish if false file is updated, true created new
   */
  private void finish(boolean finish) {
    IJ.showStatus("BOA-FINISHING");
    YesNoCancelDialog ync;
    File testF;
    boolean saveStats = true; // indicate if stQP file should be saved separatelly.
    LOGGER.debug(qState.segParam.toString());
    for (SnakeHandler sh : qState.nest.getHandlers()) {
      sh.findLastFrame(); // make sure that endFrame points good frame
    }
    imageGroup.getOrgIpl().deleteRoi(); // clean all roi for qState.nest.analyse
    if (qState.boap.saveSnake) {
      try {
        // check whether there is case saved and warn user
        testF = new File(qState.boap.deductNewParamFileName());
        // to check only once
        boolean testFileExists = testF.exists() && !testF.isDirectory();
        LOGGER.trace("Test for QCONF: " + testF.toString());
        // this field is set on loading of QCONF thus BOA will ask to save in the same
        // folder
        // show dialog if we are in create mode OR QCONF does not exist (user clicked update withour
        // saving first)
        if (finish || !testFileExists) {
          String saveIn = BOA_.qState.boap.getOutputFileCore().getParent();
          SaveDialog sd = new SaveDialog("Save segmentation data...", saveIn,
                  BOA_.qState.boap.getFileName() + FileExtensions.newConfigFileExt, "");
          if (sd.getFileName() == null) {
            BOA_.log("Save canceled");
            return;
          }
          // This initialize various filenames that can be accessed by other modules (also qconf)
          BOA_.qState.boap.setOutputFileCore(sd.getDirectory() + sd.getFileName());
          testF = new File(qState.boap.deductNewParamFileName());
          testFileExists = testF.exists() && !testF.isDirectory();
        }

        if (testFileExists) {
          ync = new YesNoCancelDialog(window, "Save Segmentation",
                  QuimpToolsCollection.stringWrap("You are about to override previous results ("
                          + testF.toString() + "). Is it ok?", QuimP.LINE_WRAP));
          if (!ync.yesPressed()) {
            return;
          }
        }
        // write operations
        // blocked by #263, enabled by 228
        if (QuimP.newFileFormat.get() == false) {
          qState.nest.writeSnakes(); // write snPQ file (if any snake) and paQP
          saveStats = true; // write also stQP file

        }
        // if (qState.nest.writeSnakes()) { // write snPQ file (if any snake) and paQP
        // write stQP file and fill outFile used later
        List<CellStatsEval> ret =
                qState.nest.analyse(imageGroup.getOrgIpl().duplicate(), saveStats);
        // auto save plugin config (but only if there is at least one snake)
        if (!qState.nest.isVacant()) {
          // Create Serialization object with extra info layer
          Serializer<SnakePluginList> s;
          s = new Serializer<>(qState.snakePluginList, quimpInfo);
          s.setPretty(); // set pretty format
          s.save(qState.boap.deductFilterFileName());
          s = null; // remove
          // Dump BOAState object in new format
          DataContainer dt = new DataContainer(); // create container
          dt.BOAState = qState; // assign boa state to correct field
          // extract relevant data from CellStat
          dt.Stats = new StatsCollection();
          dt.Stats.copyFromCellStat(ret); // StatsHandler is initialized here.
          Serializer<DataContainer> n = new Serializer<>(dt, quimpInfo);
          if (qState.boap.savePretty) {
            n.setPretty();
          }
          n.save(qState.boap.deductNewParamFileName());
          n = null;
          BOA_.log("Updated file " + BOA_.qState.boap.deductNewParamFileName());
        } else {
          BOA_.log("Nest empty. Nothing saved.");
          JOptionPane.showMessageDialog(window,
                  QuimpToolsCollection.stringWrap(
                          "There are not any cell segmented! Nothing has been saved.",
                          QuimP.LINE_WRAP),
                  "Info", JOptionPane.INFORMATION_MESSAGE);
        }
      } catch (IOException e) {
        IJ.error("Problem with saving files", e.getMessage());
        LOGGER.debug(e.getMessage(), e);
        return;
      }
    }
  }

  /**
   * Action for Quit button Set BOA_.running static field to false and close the window.
   * 
   */
  void quit() {
    BOA_.log("Finish: Exiting BOA...");
    imageGroup.makeContourImage();
    BOA_.isBoaRunning = false;
    qState.nest = null; // remove from memory
    imageGroup.getOrgIpl().setOverlay(new Overlay()); // clear overlay
    new StackWindow(imageGroup.getOrgIpl());

    window.setImage(new ImagePlus());// remove link to window
    window.close();
  }

}

/**
 * Hold, manipulate and draw on images.
 * 
 */
class ImageGroup {
  // paths - snake drawn as it contracts
  // contour - final snake drawn
  // org - original image, kept clean

  private ImagePlus orgIpl;
  private ImagePlus pathsIpl; // , contourIpl;
  private ImageStack orgStack;
  private ImageStack pathsStack; // , contourStack;
  private ImageProcessor orgIp;
  private ImageProcessor pathsIp; // , contourIp;
  private Overlay overlay;
  private Nest nest;
  private int iplWidth;
  private int iplHeight;
  private int iplStack;

  private static final Logger LOGGER = LoggerFactory.getLogger(ImageGroup.class.getName());

  /**
   * Constructor.
   * 
   * @param ipl current image opened in IJ
   * @param n Nest object associated with BOA
   */
  public ImageGroup(ImagePlus ipl, Nest n) {
    nest = n;
    // create two new stacks for drawing

    // image set up
    orgIpl = ipl;
    orgIpl.setSlice(1);
    orgIpl.getCanvas().unzoom();
    orgIpl.getCanvas().getMagnification();

    orgStack = orgIpl.getStack();
    orgIp = orgStack.getProcessor(1);

    iplWidth = orgIp.getWidth();
    iplHeight = orgIp.getHeight();
    iplStack = orgIpl.getStackSize();

    // set up blank path image
    pathsIpl = NewImage.createByteImage("Node Paths", iplWidth, iplHeight, iplStack,
            NewImage.FILL_BLACK);
    pathsStack = pathsIpl.getStack();
    pathsIpl.setSlice(1);
    pathsIp = pathsStack.getProcessor(1);

    setIpSliceAll(1);
    setProcessor(1);
  }

  /**
   * Sets new Nest object associated with displayed image.
   * 
   * <p>Used after loading new BOAState
   * 
   * @param newNest new Nest
   */
  public void updateNest(Nest newNest) {
    nest = newNest;
  }

  public ImagePlus getOrgIpl() {
    return orgIpl;
  }

  /**
   * Return IP with drawn paths of snakes.
   * 
   * @return ImageProcessor with paths
   * @see BOAState.SegParam#showPaths
   */
  public ImagePlus getPathsIpl() {
    return pathsIpl;
  }

  public ImageProcessor getOrgIp() {
    return orgIp;
  }

  /**
   * Plots snakes on current frame.
   * 
   * <p>Depending on configuration this method can plot:
   * <ol>
   * <li>Snake after segmentation, without processing by plugins
   * <li>Snake after segmentation and after processing by all active plugins
   * </ol>
   * It assign also last created Snake to ViewUpdater. This Snake can be accessed by plugin for
   * previewing purposes. If last Snake has been deleted, null is assigned or before last Snake
   * 
   * <p>Used when there is a need of redrawing screen because of new data
   * 
   * @param frame Current frame
   */
  public void updateOverlay(int frame) {
    LOGGER.trace("Update overlay for frame " + frame);
    SnakeHandler snakeH;
    Snake snake;
    Snake back;
    int x;
    int y;
    TextRoi text;
    Roi r;
    overlay = new Overlay();
    BOA_.viewUpdater.connectSnakeObject(null); //
    Color snakeColor = Color.YELLOW;
    for (int i = 0; i < nest.size(); i++) {
      snakeH = nest.getHandler(i);
      if (snakeH.isSnakeHandlerFrozen()) {
        snakeColor = Color.BLUE;
      } else {
        snakeColor = Color.YELLOW;
      }
      if (snakeH.isStoredAt(frame)) { // is there a snake at iplStack?

        // plot segmented snake
        if (BOA_.qState.boap.isProcessedSnakePlotted == true) {
          back = snakeH.getBackupSnake(frame); // original unmodified snake
          // Roi r = snake.asRoi();
          r = back.asFloatRoi();
          r.setStrokeColor(Color.RED);
          overlay.add(r);
        }
        // remember instance of segmented snake for plugins (last created)
        BOA_.viewUpdater.connectSnakeObject(snakeH.getBackupSnake(frame));
        // plot segmented and filtered snake
        snake = snakeH.getStoredSnake(frame); // processed by plugins
        // Roi r = snake.asRoi();
        r = snake.asFloatRoi();
        r.setStrokeColor(snakeColor);
        overlay.add(r);
        x = (int) Math.round(snake.getCentroid().getX()) - 15;
        y = (int) Math.round(snake.getCentroid().getY()) - 15;
        text = new TextRoi(x, y, "   " + snake.getSnakeID());
        overlay.add(text);

        // draw centre point
        PointRoi pointR =
                new PointRoi((int) snake.getCentroid().getX(), (int) snake.getCentroid().getY());
        overlay.add(pointR);

        // draw head node
        if (BOA_.qState.boap.isHeadPlotted == true) {
          // base point = 0 node
          Point2d bp = new Point2d(snake.getHead().getX(), snake.getHead().getY());

          // Plot Arrow mounted in 0 node and pointing direction of Snake
          Vector2d dir =
                  new Vector2d(snake.getHead().getNext().getNext().getNext().getX() - bp.getX(),
                          snake.getHead().getNext().getNext().getNext().getY() - bp.getY());
          FloatPolygon fp = GraphicsElements.plotArrow(dir, bp, 12.0f, 0.3f);
          PolygonRoi polygonR = new PolygonRoi(fp, Roi.POLYGON);
          polygonR.setStrokeColor(Color.MAGENTA);
          polygonR.setFillColor(Color.MAGENTA);
          overlay.add(polygonR);

          // plot circle on head
          FloatPolygon fp1 = GraphicsElements.getCircle(bp, 10);
          PolygonRoi polyginR1 = new PolygonRoi(fp1, Roi.POLYGON);
          polyginR1.setStrokeColor(Color.GREEN);
          polyginR1.setFillColor(Color.GREEN);
          overlay.add(polyginR1);
        }
      } else {
        BOA_.viewUpdater.connectSnakeObject(null);
      }
    }
    orgIpl.setOverlay(overlay);
  }

  /**
   * Updates IJ to current frame. Causes that updateSliceSelector() is called.
   * 
   * <p>USed when there is a need to move to other frame programmatically.
   * 
   * @param frame current frame
   * 
   */
  public void updateToFrame(int frame) {
    clearPaths(frame);
    setProcessor(frame);
    setIpSliceAll(frame);
  }

  public void clearOverlay() {
    // overlay = new Overlay();
    orgIpl.setOverlay(null);
  }

  /**
   * Set internal field to currently processed ImageProcessor.
   * 
   * <p>Those fields are used by e.g. {@link BOA_#runBoa(int, int)} during computations.
   * 
   * @param i current frame
   */
  public final void setProcessor(int i) {
    orgIp = orgStack.getProcessor(i);
    pathsIp = pathsStack.getProcessor(i);
    // System.out.println("\n1217 Proc set to : " + i);
  }

  /**
   * Set slice in stack. Call updateSliceSelector callback only if i != current frame.
   * 
   * @param i slice number
   */
  public final void setIpSliceAll(int i) {
    // set slice on all images
    pathsIpl.setSlice(i);
    orgIpl.setSlice(i);
  }

  /**
   * Erase all paths stored during segmentation.
   * 
   * <p>Paths are drawn in separate internal ImagePorcessot that is then displayed in place of
   * original image in BOA.
   * 
   * @param fromFrame start frame to erase from
   * 
   * @see #getPathsIpl()
   * @see BOAState.SegParam#showPaths
   */
  public void clearPaths(int fromFrame) {
    for (int i = fromFrame; i <= BOA_.qState.boap.getFrames(); i++) {
      pathsIp = pathsStack.getProcessor(i);
      pathsIp.setValue(0);
      pathsIp.fill();
    }
    pathsIp = pathsStack.getProcessor(fromFrame);
  }

  /**
   * Draw Snake contraction paths in internal stack.
   * 
   * <p>Internal stack with paths can be displayed by setting {@link BOAState.SegParam#showPaths}
   * 
   * @param snake snake to draw
   * @param frame current frame
   */
  public void drawPath(Snake snake, int frame) {
    pathsIp = pathsStack.getProcessor(frame);
    drawSnake(pathsIp, snake, false);
  }

  private void drawSnake(final ImageProcessor ip, final Snake snake, boolean contrast) {
    // draw snake
    int x;
    int y;
    int intensity;

    Node n = snake.getHead();
    do {
      x = (int) (n.getPoint().getX());
      y = (int) (n.getPoint().getY());

      if (!contrast) {
        intensity = 245;
      } else {
        // paint as black or white for max contrast
        if (ip.getPixel(x, y) > 127) {
          intensity = 10;
        } else {
          intensity = 245;
        }
      }
      // for colour:
      // if(boap.drawColor) intensity = n.colour.getColorInt();

      if (BOA_.qState.boap.getHeight() > 800) {
        drawPixel(x, y, intensity, true, ip);
      } else {
        drawPixel(x, y, intensity, false, ip);
      }
      n = n.getNext();
    } while (!n.isHead());
  }

  private void drawPixel(int x, int y, int intensity, boolean fat, ImageProcessor ip) {
    ip.putPixel(x, y, intensity);
    if (fat) {
      ip.putPixel(x + 1, y, intensity);
      ip.putPixel(x + 1, y + 1, intensity);
      ip.putPixel(x, y + 1, intensity);
      ip.putPixel(x - 1, y + 1, intensity);
      ip.putPixel(x - 1, y, intensity);
      ip.putPixel(x - 1, y - 1, intensity);
      ip.putPixel(x, y - 1, intensity);
      ip.putPixel(x + 1, y - 1, intensity);
    }
  }

  void makeContourImage() {
    ImagePlus contourIpl = NewImage.createByteImage("Contours", iplWidth, iplHeight, iplStack,
            NewImage.FILL_BLACK);
    ImageStack contourStack = contourIpl.getStack();
    contourIpl.setSlice(1);
    ImageProcessor contourIp;

    for (int i = 1; i <= BOA_.qState.boap.getFrames(); i++) { // copy original
      orgIp = orgStack.getProcessor(i);
      contourIp = contourStack.getProcessor(i);
      contourIp.copyBits(orgIp, 0, 0, Blitter.COPY);
    }

    drawCellRois(contourStack);
    new ImagePlus(orgIpl.getTitle() + "_Segmentation", contourStack).show();

  }

  /**
   * Zoom current view to snake with snakeID.
   * 
   * <p>If snake is not found nothing happens.
   * 
   * @param ic Current view
   * @param frame Frame the Snake is looked in
   * @param snakeID ID of Snake one looks for
   */
  void zoom(final ImageCanvas ic, int frame, int snakeID) {
    LOGGER.trace("Zoom to frame: " + frame + " ID " + snakeID);
    if (nest.isVacant() || snakeID < 0) {
      return; // negative id or empty nest
    }
    SnakeHandler snakeH;
    Snake snake;

    try {
      snakeH = nest.getHandlerofId(snakeID);// snakeID, not index
      if (snakeH != null && snakeH.isStoredAt(frame)) {
        snake = snakeH.getStoredSnake(frame);
      } else {
        return;
      }
    } catch (IndexOutOfBoundsException | IllegalArgumentException e) {
      LOGGER.debug(e.getMessage(), e);
      return;
    }

    Rectangle r = snake.getBounds();
    int border = 40;

    // add border (10 either way)
    r.setBounds(r.x - border, r.y - border, r.width + border * 2, r.height + border * 2);

    // correct r's aspect ratio
    double icAspect = (double) ic.getWidth() / (double) ic.getHeight();
    double rectAspect = r.getWidth() / r.getHeight();
    int newDim; // new dimenesion size

    if (icAspect < rectAspect) {
      // too short
      newDim = (int) (r.getWidth() / icAspect);
      r.y = r.y - ((newDim - r.height) / 2); // move snake to center
      r.height = newDim;
    } else {
      // too thin
      newDim = (int) (r.getHeight() * icAspect);
      r.x = r.x - ((newDim - r.width) / 2); // move snake to center
      r.width = newDim;
    }

    // System.out.println("ic " + icAspect + ". rA: " + r.getWidth() /
    // r.getHeight());

    double newMag;

    newMag = (double) ic.getHeight() / r.getHeight(); // mag required

    ic.setMagnification(newMag);
    Rectangle sr = ic.getSrcRect();
    sr.setBounds(r);

    ic.repaint();

  }

  void unzoom(final ImageCanvas ic) {
    // Rectangle sr = ic.getSrcRect();
    // sr.setBounds(0, 0, boap.WIDTH, boap.HEIGHT);
    ic.unzoom();
    // ic.setMagnification(orgMag);
    // ic.repaint();
  }

  /**
   * Produces final image with Snake outlines after finishing BOA.
   * 
   * <p>This is ImageJ image with flatten Snake contours.
   * 
   * @param stack Stack to plot in
   */
  void drawCellRois(final ImageStack stack) {
    Snake snake;
    SnakeHandler snakeH;
    ImageProcessor ip;

    int x;
    int y;
    for (int s = 0; s < nest.size(); s++) {
      snakeH = nest.getHandler(s);
      for (int i = 1; i <= BOA_.qState.boap.getFrames(); i++) {
        if (snakeH.isStoredAt(i)) {
          snake = snakeH.getStoredSnake(i);
          ip = stack.getProcessor(i);
          ip.setColor(255);
          ip.draw(snake.asFloatRoi());
          x = (int) Math.round(snake.getHead().getX()) - 15;
          y = (int) Math.round(snake.getHead().getY()) - 15;
          ip.moveTo(x, y);
          ip.drawString("   " + snake.getSnakeID());
          LOGGER.trace("Snake head is at: " + snake.getHead().toString());
        }
      }
    }
  }
}