BOAState.java

package com.github.celldynamics.quimp;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.github.celldynamics.quimp.filesystem.FileExtensions;
import com.github.celldynamics.quimp.filesystem.IQuimpSerialize;
import com.github.celldynamics.quimp.plugin.ParamList;
import com.github.celldynamics.quimp.plugin.binaryseg.BinarySegmentation_;
import com.github.celldynamics.quimp.plugin.engine.PluginFactory;
import com.github.celldynamics.quimp.utils.QuimpToolsCollection;

import ij.ImagePlus;
import ij.io.FileInfo;

/*
 * //!>
 * @startuml doc-files/BOAState_UML.png
 * BOAState *-- "1" BOAp
 * BOAState *-- "1" SegParam
 * BOAState <|.. IQuimpSerialize
 * BOAState : +boap
 * BOAState : +SegParam
 * BOAState : -segParamSnapshots
 * BOAState : +SnakePluginList
 * BOAState : +Nest
 * BOAState : ... 
 * BOAState : +beforeSerialize()
 * BOAState : +afterSerialize()
 * BOAState : +store()
 * BOAState : +storeOnlyEdited()
 * SegParam : +nodeList
 * SegParam : +imageForce
 * SegParam : +equals()
 * SegParam : +hashCode()
 * SegParam : +setDefaults()
 * SegParam : ...()
 * SegParam : ...
 * IQuimpSerialize : +beforeSerialize()
 * IQuimpSerialize : +afterSerialize()
 * BOAp : -imageScale
 * BOAp : -scaleAdjusted
 * BOAp : +frame
 * BOAp : ~zoom
 * BOAp : ...
 * BOAp : ...()
 * @enduml
 * //!<
 */
/**
 * Hold current BOA state that can be serialized.
 * 
 * <p>This class is composed from two inner classes:
 * <ul>
 * <li>BOAp - holds internal state of BOA plugin, maintained mainly for compatibility reasons
 * <li>SegParam - holds segmentation parameters, exposed to UI
 * </ul>
 * 
 * <p>Moreover there are several fields related to new features of QuimP like storing internal state
 * for every frame separately or SnakePlugins.<br>
 * 
 * <img src="doc-files/BOAState_UML.png"/><br>
 * 
 * @author p.baniukiewicz
 * @see Serializer
 */
public class BOAState implements IQuimpSerialize {

  private static final Logger LOGGER = LoggerFactory.getLogger(BOAState.class.getName());
  /**
   * Reference to segmentation parameters. Holds current parameters.
   * 
   * <p>On every change of BOA state it is stored as copy in segParamSnapshots for current frame.
   * This is why that field is transient
   * 
   * @see com.github.celldynamics.quimp.BOA_#run(String)
   * @see <a href=
   *      "http://www.trac-wsbc.linkpc.net:8080/trac/QuimP/wiki/ConfigurationHandling">ConfigurationHandling</a>
   */
  public transient SegParam segParam;
  /**
   * Reference to old BOAp class, keeps internal state of BOA.
   */
  public BOAp boap;
  /**
   * Instance of binary segmentation plugin that converts BW masks into snakes.
   * 
   * <p>This is regular plugin but handled separately from SnakePlugins and it is not provided as
   * external jar
   */
  public transient BinarySegmentation_ binarySegmentationPlugin;
  /**
   * Configuration of BinarySegmentation plugin if it was used. Used during saving boa state.
   */
  ParamList binarySegmentationParam;
  /**
   * Keep snapshots of SegParam objects for every frame separately.
   */
  private ArrayList<SegParam> segParamSnapshots;
  /**
   * Keep snapshots of SnakePluginList objects for every frame separately.
   * 
   * <p>Plugin configurations are stored as well (but without plugin references)
   */
  public ArrayList<SnakePluginList> snakePluginListSnapshots;
  /**
   * List of plugins selected in plugin stack and information if they are active or not. This field
   * is not serializable because snakePluginListSnapshots keeps configurations for
   * every frame.
   * 
   * <p>Holds current parameters as the main object not referenced in BOAp On every change of BOA
   * state it is stored as copy in snakePluginListSnapshots for current frame. This is why that
   * field is transient
   * 
   * @see com.github.celldynamics.quimp.SnakePluginList
   * @see com.github.celldynamics.quimp.BOA_#run(String)
   * @see com.github.celldynamics.quimp.BOAState#store(int)
   */
  public transient SnakePluginList snakePluginList;
  /**
   * Reference to Nest, which is serializable as well. This is main object not referenced in other
   * parts of QuimP
   */
  public Nest nest;
  /**
   * Store information whether for current frame button <b>Edit</b> was used. Do not indicate that
   * any of Snakes was edited.
   */
  public ArrayList<Boolean> isFrameEdited;

  /**
   * Hold user parameters for segmentation algorithm.
   * 
   * <p>Most of those parameters are available from BOA user menu. This class supports cloning and
   * comparing.
   * 
   * @author p.baniukiewicz
   * @see com.github.celldynamics.quimp.BOAState
   */
  class SegParam {
    /**
     * Number of nodes on ROI edge.
     * 
     * <p>Cell segmentation parameter. Check user manual or our publications for details.
     */
    private double nodeRes;
    /**
     * Distance to blow up chain.
     * 
     * <p>Check user manual or our publications for details.
     */
    public int blowup;
    /**
     * Critical velocity.
     * 
     * <p>Cell segmentation parameter. Check user manual or our publications for details.
     */
    public double vel_crit;
    /**
     * Central force.
     * 
     * <p>Cell segmentation parameter. Check user manual or our publications for details.
     */
    public double f_central;
    /**
     * Image force.
     * 
     * <p>Cell segmentation parameter. Check user manual or our publications for details.
     */
    public double f_image;
    /**
     * Max iterations per contraction.
     * 
     * <p>Cell segmentation parameter. Check user manual or our publications for details.
     */
    public int max_iterations;
    /**
     * Sample tan.
     * 
     * <p>Cell segmentation parameter. Check user manual or our publications for details.
     */
    public int sample_tan;
    /**
     * Sample norm.
     * 
     * <p>Cell segmentation parameter. Check user manual or our publications for details.
     */
    public int sample_norm;
    /**
     * Contraction force.
     * 
     * <p>Cell segmentation parameter. Check user manual or our publications for details.
     */
    public double f_contract;
    /**
     * Final shrink.
     * 
     * <p>Cell segmentation parameter. Check user manual or our publications for details.
     */
    public double finalShrink;
    // Switch Params
    /**
     * Next contraction begins with prev chain.
     */
    public boolean use_previous_snake;
    /**
     * Decide whether to show paths on screen.
     * 
     * <p>Cell segmentation parameter. Check user manual or our publications for details.
     */
    public boolean showPaths;
    /**
     * Whether to act as an expanding snake.
     * 
     * <p>Visualisation option parameter. Check user manual or our publications for details.
     * 
     * @deprecated Not sure what it actually does, will be disabled in GUI but keep here for
     *             compatibility
     */
    public boolean expandSnake;
    /**
     * Min distance between nodes.
     * 
     * <p>Cell segmentation parameter.
     */
    private double min_dist;
    /**
     * Max distance between nodes.
     * 
     * <p>Cell segmentation parameter.
     */
    private double max_dist;
    /**
     * This is sign for forces that eventually contracts or expands the snake.
     * 
     * <p>true value is for contraction (standard behaviour).
     * 
     * @see #reverseForces()
     */
    public boolean contractingDirection = true;

    /**
     * Copy constructor.
     * 
     * @param src object to copy
     */
    public SegParam(final SegParam src) {
      this.nodeRes = src.nodeRes;
      this.blowup = src.blowup;
      this.vel_crit = src.vel_crit;
      this.f_central = src.f_central;
      this.f_image = src.f_image;
      this.max_iterations = src.max_iterations;
      this.sample_tan = src.sample_tan;
      this.sample_norm = src.sample_norm;
      this.f_contract = src.f_contract;
      this.finalShrink = src.finalShrink;
      this.use_previous_snake = src.use_previous_snake;
      this.showPaths = src.showPaths;
      this.expandSnake = src.expandSnake;
      this.min_dist = src.min_dist;
      this.max_dist = src.max_dist;
      this.contractingDirection = src.contractingDirection;
    }

    /**
     * Sets default values of parameters.
     */
    public SegParam() {
      setDefaults();
      // defaults for GUI settings
      showPaths = false;
      use_previous_snake = true; // next contraction begins with last chain
      expandSnake = false; // set true to act as an expanding snake

    }

    /*
     * (non-Javadoc)
     * 
     * @see java.lang.Object#hashCode()
     */
    @Override
    public int hashCode() {
      final int prime = 31;
      int result = 1;
      result = prime * result + blowup;
      result = prime * result + (expandSnake ? 1231 : 1237);
      long temp;
      temp = Double.doubleToLongBits(f_central);
      result = prime * result + (int) (temp ^ (temp >>> 32));
      temp = Double.doubleToLongBits(f_contract);
      result = prime * result + (int) (temp ^ (temp >>> 32));
      temp = Double.doubleToLongBits(max_dist);
      result = prime * result + (int) (temp ^ (temp >>> 32));
      temp = Double.doubleToLongBits(min_dist);
      result = prime * result + (int) (temp ^ (temp >>> 32));
      temp = Double.doubleToLongBits(f_image);
      result = prime * result + (int) (temp ^ (temp >>> 32));
      temp = Double.doubleToLongBits(finalShrink);
      result = prime * result + (int) (temp ^ (temp >>> 32));
      result = prime * result + max_iterations;
      temp = Double.doubleToLongBits(nodeRes);
      result = prime * result + (int) (temp ^ (temp >>> 32));
      result = prime * result + sample_norm;
      result = prime * result + sample_tan;
      result = prime * result + (showPaths ? 1231 : 1237);
      result = prime * result + (use_previous_snake ? 1231 : 1237);
      temp = Double.doubleToLongBits(vel_crit);
      result = prime * result + (int) (temp ^ (temp >>> 32));
      result = prime * result + (contractingDirection ? 1231 : 1237);
      return result;
    }

    /*
     * (non-Javadoc)
     * 
     * @see java.lang.Object#equals(java.lang.Object)
     */
    @Override
    public boolean equals(Object obj) {
      if (this == obj) {
        return true;
      }
      if (obj == null) {
        return false;
      }
      if (!(obj instanceof SegParam)) {
        return false;
      }
      SegParam other = (SegParam) obj;
      if (blowup != other.blowup) {
        return false;
      }
      if (expandSnake != other.expandSnake) {
        return false;
      }
      if (Double.doubleToLongBits(f_central) != Double.doubleToLongBits(other.f_central)) {
        return false;
      }
      if (Double.doubleToLongBits(min_dist) != Double.doubleToLongBits(other.min_dist)) {
        return false;
      }
      if (Double.doubleToLongBits(max_dist) != Double.doubleToLongBits(other.max_dist)) {
        return false;
      }
      if (Double.doubleToLongBits(f_contract) != Double.doubleToLongBits(other.f_contract)) {
        return false;
      }
      if (Double.doubleToLongBits(f_image) != Double.doubleToLongBits(other.f_image)) {
        return false;
      }
      if (Double.doubleToLongBits(finalShrink) != Double.doubleToLongBits(other.finalShrink)) {
        return false;
      }
      if (max_iterations != other.max_iterations) {
        return false;
      }
      if (Double.doubleToLongBits(nodeRes) != Double.doubleToLongBits(other.nodeRes)) {
        return false;
      }
      if (sample_norm != other.sample_norm) {
        return false;
      }
      if (sample_tan != other.sample_tan) {
        return false;
      }
      if (showPaths != other.showPaths) {
        return false;
      }
      if (use_previous_snake != other.use_previous_snake) {
        return false;
      }
      if (Double.doubleToLongBits(vel_crit) != Double.doubleToLongBits(other.vel_crit)) {
        return false;
      }
      if (contractingDirection != other.contractingDirection) {
        return false;
      }
      return true;
    }

    /**
     * Return nodeRes.
     * 
     * @return nodeRes field
     */
    public double getNodeRes() {
      return nodeRes;
    }

    /**
     * Set nodeRes field and calculate <tt>min_dist</tt> and <tt>max_dist</tt>.
     * 
     * @param d resolution
     */
    public void setNodeRes(double d) {
      nodeRes = d;
      if (nodeRes < 1) {
        min_dist = 1; // min distance between nodes
        max_dist = 2.3; // max distance between nodes
        return;
      }
      min_dist = nodeRes; // min distance between nodes
      max_dist = nodeRes * 1.9; // max distance between nodes
    }

    /**
     * Set default parameters for contour matching algorithm.
     * 
     * <p>These parameters are external - available for user to set in GUI.
     */
    public void setDefaults() {
      setNodeRes(6.0);
      blowup = 20; // distance to blow up chain
      vel_crit = 0.005;
      f_central = 0.04;
      f_image = 0.2; // image force
      max_iterations = 4000; // max iterations per contraction
      sample_tan = 4;
      sample_norm = 12;
      f_contract = 0.04;
      finalShrink = 3d;
      contractingDirection = true;
    }

    /**
     * Gets the max dist.
     *
     * @return the max dist
     */
    public double getMax_dist() {
      return max_dist;
    }

    /**
     * Gets the min dist.
     *
     * @return the min dist
     */
    public double getMin_dist() {
      return min_dist;
    }

    /*
     * (non-Javadoc)
     * 
     * @see java.lang.Object#toString()
     */
    @Override
    public String toString() {
      return "SegParam [nodeRes=" + nodeRes + ", blowup=" + blowup + ", velCrit=" + vel_crit
              + ", centralForce=" + f_central + ", imageForce=" + f_image + ", maxIterations="
              + max_iterations + ", sampleTan=" + sample_tan + ", sampleNorm=" + sample_norm
              + ", contractForce=" + f_contract + ", finalShrink=" + finalShrink
              + ", use_previous_snake=" + use_previous_snake + ", showPaths=" + showPaths
              + ", expandSnake=" + expandSnake + ", min_dist=" + min_dist + ", max_dist=" + max_dist
              + " contractingDirection=" + contractingDirection + "]";
    }

    /**
     * Reverse forces and then changes direction of snake contracting.
     * 
     * <p>Related to {@link #contractingDirection}.
     */
    public void reverseForces() {
      if (contractingDirection == true) { // default contracting
        finalShrink = Math.abs(finalShrink);
        f_image = Math.abs(f_image);
        f_central = Math.abs(f_central);
        // vel_crit = Math.abs(vel_crit);
        blowup = 20;// Math.abs(blowup);
      } else { // expand - reversed forces
        finalShrink = -1.0 * Math.abs(finalShrink); // if user set - in spinner, we want it overrid
        f_image = -1.0 * Math.abs(f_image);
        f_central = -1.0 * Math.abs(f_central);
        // vel_crit = -1.0 * Math.abs(vel_crit);
        blowup = -1;// Math.abs(blowup);
      }
    }

  } // end of SegParam

  /**
   * Holds parameters defining snake and controlling contour matching algorithm.
   * 
   * <p>BOAp is static class contains internal as well as external parameters used to define snake
   * and to control contour matching algorithm. There are also several basic get/set methods for
   * accessing selected parameters, setting default
   * {@link com.github.celldynamics.quimp.BOAState.SegParam#setDefaults() values} and
   * writing/reading
   * these (external) parameters to/from disk. File format used for storing data in files is
   * defined at {@link QParams} class.
   * 
   * <p>External parameters are those related to algorithm options whereas internal are those
   * related
   * to internal settings of algorithm, GUI and whole plugin
   * 
   * <p>This class is shared among different QuimP components.
   * 
   * @author rtyson
   * @see com.github.celldynamics.quimp.QParams
   */
  public class BOAp {
    /**
     * handle to original file obtained from IJ (usually image opened).
     * 
     * <p>This filed is serialised for further verification of QCONF and image matching but not
     * restored on load to not overwrite currently opened image name.
     */
    private File orgFile;
    /**
     * Corename for output, initially contains path and name without extension from orgFile and
     * without cell number.
     * 
     * <p>Can be changed by user on save Change of this field causes change of the
     * <tt>fileName</tt>
     */
    private File outputFileCore;
    /**
     * <tt>outputFileCore</tt> but without path and extension.
     */
    private String fileName;
    /**
     * read in parameter file.
     */
    transient QParams readQp;
    // internal parameters
    /**
     * Maximum number of nodes (% of starting nodes).
     */
    int NMAX; // name related to QCONF file do not change

    /**
     * The delta t.
     */
    double delta_t;

    /**
     * The sensitivity.
     */
    double sensitivity;

    /**
     * The f friction.
     */
    double f_friction;
    /**
     * Number of frames in stack.
     */
    private int FRAMES; // name related to QCONF file do not change
    private int WIDTH; // name related to QCONF file do not change
    private int HEIGHT; // name related to QCONF file do not change
    /**
     * Cut loops in chain every X frames.
     */
    int cut_every;
    /**
     * output old QuimP format.
     */
    boolean oldFormat;
    /**
     * save snake data.
     */
    boolean saveSnake;

    /**
     * distance between centroids at which contact is tested for.
     */
    double proximity;
    /**
     * Proximity of nodes to freeze when blowing up.
     */
    double proxFreeze;

    /**
     * The saved one.
     */
    boolean savedOne;
    /**
     * Use json pretty format.
     */
    boolean savePretty = true;

    /**
     * Current frame, CustomStackWindow.updateSliceSelector().
     */
    public int frame;
    /**
     * Snake selected in zoom selector, negative value if 100% view.
     */
    public int snakeToZoom = -1;

    /**
     * The single image.
     */
    boolean singleImage;

    /**
     * The params exist.
     */
    String paramsExist; // on startup check if defaults are needed to set

    /**
     * The zoom.
     */
    boolean zoom;

    /**
     * The do delete.
     */
    boolean doDelete;
    /**
     * true if we are in freeze mode."
     */
    transient boolean doFreeze;

    /**
     * The do delete seg.
     */
    boolean doDeleteSeg;
    /**
     * is select a cell for editing active.
     */
    boolean editMode;
    /**
     * currently editing cell iD. -1 if not editing
     */
    int editingID;

    /**
     * The use sub pixel.
     */
    boolean useSubPixel = true;
    /**
     * use to test how many times a method is called.
     */
    int callCount;

    private double imageScale; // scale of image read from ip
    private boolean scaleAdjusted = false; // true when adjusted in constructor

    /**
     * Get scale of the image.
     * 
     * @return the imageScale
     */
    public double getImageScale() {
      return imageScale;
    }

    /**
     * Set scale of teh image.
     * 
     * @param imageScale the imageScale to set
     */
    public void setImageScale(double imageScale) {
      if (imageScale == 0) {
        this.imageScale = 1;
        this.scaleAdjusted = true;
      } else {
        this.imageScale = imageScale;
      }
    }

    /**
     * Get scaleAdjusted.
     * 
     * @return the scaleAdjusted
     */
    public boolean isScaleAdjusted() {
      return scaleAdjusted;
    }

    private double imageFrameInterval;
    private boolean fIAdjusted = false;

    /**
     * Get imageFrameInterval.
     * 
     * @return the imageFrameInterval
     */
    public double getImageFrameInterval() {
      return imageFrameInterval;
    }

    /**
     * Set imageFrameInterval.
     * 
     * @param imageFrameInterval the imageFrameInterval to set
     */
    public void setImageFrameInterval(double imageFrameInterval) {
      if (imageFrameInterval == 0) {
        this.imageFrameInterval = 1;
        this.fIAdjusted = true;
      } else {
        this.imageFrameInterval = imageFrameInterval;
      }
    }

    /**
     * Get fIAdjusted.
     * 
     * @return the fIAdjusted
     */
    public boolean isfIAdjusted() {
      return fIAdjusted;
    }

    /**
     * Default constructor.
     */
    public BOAp() {
      savedOne = false;
      // nestSize = 0;
      // internal parameters
      NMAX = 250; // maximum number of nodes (% of starting nodes)
      delta_t = 1.;
      sensitivity = 0.5;
      cut_every = 8; // cut loops in chain every X interations
      oldFormat = false; // output old QuimP format?
      saveSnake = true; // save snake data
      proximity = 150; // distance between centroids at which contact is tested for
      proxFreeze = 1; // proximity of nodes to freeze when blowing up
      f_friction = 0.6;
      doDelete = false;
      doDeleteSeg = false;
      zoom = false;
      editMode = false;
      editingID = -1;
      callCount = 0;
      frame = 1;
      savePretty = true;
    }

    /**
     * Plot or not snakes after processing by plugins.
     * 
     * <p>If true both snakes, after segmentation and after filtering are plotted.
     */
    boolean isProcessedSnakePlotted = true;

    /**
     * Define if first node of Snake (head) is plotted or not.
     */
    boolean isHeadPlotted = false;

    /**
     * If cell is zoomed, all parameter changes influence only this cell. (UI)
     */
    boolean isZoomFreeze = false;

    /**
     * When any plugin fails this field defines how QuimP should behave.
     * 
     * <p>When it is true QuimP breaks process of segmentation and do not store filtered snake in
     * SnakeHandler.
     * TODO Implement this feature
     * 
     * @see <a href="http://www.trac-wsbc.linkpc.net:8080/trac/QuimP/ticket/81">ticket#81</a>
     */
    boolean stopOnPluginError = true;

    /**
     * Initialize internal parameters of BOA plugin from ImagePlus.
     * 
     * <p>Most of these parameters are related to state machine of BOA. There are also parameters
     * related to internal state of Active Contour algorithm. Defaults for parameters available
     * for user are set in {@link com.github.celldynamics.quimp.BOAState.SegParam#setDefaults()}
     * 
     * @param ip Reference to segmented image passed from IJ
     */
    public void setup(final ImagePlus ip) {
      FileInfo fileinfo = ip.getOriginalFileInfo();
      if (fileinfo == null) {
        orgFile = new File(File.separator, ip.getTitle());
        setOutputFileCore(File.separator + ip.getTitle());
      } else {
        orgFile = new File(fileinfo.directory, fileinfo.fileName);
        setOutputFileCore(fileinfo.directory + orgFile.getName());
      }

      FRAMES = ip.getStackSize(); // get number of frames

      WIDTH = ip.getWidth();
      HEIGHT = ip.getHeight();
      paramsExist = "YES";

    }

    /**
     * Get FRAMES.
     * 
     * @return the fRAMES
     */
    public int getFrames() {
      return FRAMES;
    }

    /**
     * Get WIDTH.
     * 
     * @return the wIDTH
     */
    public int getWidth() {
      return WIDTH;
    }

    /**
     * Get HEIGHT.
     * 
     * @return the hEIGHT
     */
    public int getHeight() {
      return HEIGHT;
    }

    /**
     * Get orgFile - image opened in BOA.
     * 
     * @return the orgFile
     */
    public File getOrgFile() {
      return orgFile;
    }

    /**
     * Set orgFile.
     * 
     * @param orgFile the orgFile to set
     */
    public void setOrgFile(File orgFile) {
      this.orgFile = orgFile;
    }

    /**
     * Get outputFileCore.
     * 
     * @return the outputFileCore
     * @see #outputFileCore
     */
    public File getOutputFileCore() {
      return outputFileCore;
    }

    /**
     * Set outputFileCore.
     * 
     * <p>From compatibility reasons outputFileCore is File type.
     * 
     * @param outputFileCore the outputFileCore to set. should not contain extension
     */
    private void setOutputFileCore(File outputFileCore) {
      this.outputFileCore = outputFileCore;
      fileName = outputFileCore.getName();
    }

    /**
     * Set setOutputFileCore.
     * 
     * @param outputFileCore the outputFileCore to set
     */
    public void setOutputFileCore(String outputFileCore) {
      setOutputFileCore(new File(QuimpToolsCollection.removeExtension(outputFileCore)));
    }

    /**
     * Get fileName.
     * 
     * @return the fileName
     */
    public String getFileName() {
      return fileName;
    }

    /**
     * Generate Snake file name basing on ID.
     * 
     * <p>Mainly to have this in one place. Use outputFileCore that is set by user choice of output
     * 
     * @param id of Snake
     * @return Full path to file with extension
     */
    public String deductSnakeFileName(int id) {
      LOGGER.trace(getOutputFileCore().getAbsoluteFile().toString());
      return getOutputFileCore().getAbsoluteFile() + "_" + id + FileExtensions.snakeFileExt;
    }

    /**
     * Generate stats file name basing on ID.
     * 
     * <p>Mainly to have this in one place. Use outputFileCore that is set by user choice of output
     * 
     * @param id of Snake
     * @return Full path to file with extension
     */
    public String deductStatsFileName(int id) {
      return getOutputFileCore().getAbsoluteFile() + "_" + id + FileExtensions.statsFileExt;
    }

    /**
     * Generate main param file (old) name basing on ID.
     * 
     * <p>Mainly to have this in one place. Use outputFileCore that is set by user choice of output
     * 
     * @param id of Snake
     * @return Full path to file with extension
     */
    public String deductParamFileName(int id) {
      return getOutputFileCore().getAbsoluteFile() + "_" + id + FileExtensions.configFileExt;
    }

    /**
     * Generate main filter config file name.
     * 
     * <p>Mainly to have this in one place. Use outputFileCore that is set by user choice of output
     * 
     * @return Full path to file with extension
     */
    public String deductFilterFileName() {
      return getOutputFileCore().getAbsoluteFile() + FileExtensions.pluginFileExt;
    }

    /**
     * Generate main param file (new) name.
     * 
     * <p>Mainly to have this in one place. Use outputFileCore that is set by user choice of output
     * 
     * @return Full path to file with extension
     */
    public String deductNewParamFileName() {
      return getOutputFileCore().getAbsoluteFile() + FileExtensions.newConfigFileExt;
    }

  }

  /**
   * Default constructor. Should be used with care as some image properties are not initialised
   * here.
   * 
   * @see BOAState#BOAState(ImagePlus)
   */
  public BOAState() {
    boap = new BOAp(); // build BOAp
    segParam = new SegParam(); // and SegParam
    snakePluginList = new SnakePluginList();
    binarySegmentationParam = new ParamList(); // save empty list even if plugin not used
    nest = new Nest();
  }

  /**
   * Construct BOAState object for given stack size. Initializes other internal fields.
   * 
   * @param ip current image object, can be \c null. In latter case only subclasses are
   *        initialized
   */
  public BOAState(final ImagePlus ip) {
    this(ip, null, null);

  }

  /**
   * Construct full base object filling snapshots with default but valid objects.
   * 
   * @param ip current image object, can be \c null. In latter case only subclasses are
   *        initialized
   * @param pf PluginFactory used for creating plugins
   * @param vu ViewUpdater reference
   */
  public BOAState(final ImagePlus ip, final PluginFactory pf, final ViewUpdater vu) {
    this();
    snakePluginList = new SnakePluginList(BOA_.NUM_SNAKE_PLUGINS, pf, vu);
    if (ip == null) {
      return;
    }

    initializeSnapshots(ip, pf, vu);
    // set scale read from image, set also scaleAdjusted if scale from image is wrong
    boap.setImageScale(ip.getCalibration().pixelWidth);
    // set interval read from image, set also fIAdjusted if scale from image is wrong
    boap.setImageFrameInterval(ip.getCalibration().frameInterval);
    boap.setup(ip);
  }

  private void initializeSnapshots(final ImagePlus ip, final PluginFactory pf,
          final ViewUpdater vu) {
    int numofframes = ip.getStackSize();
    // fill snaphots with default values
    segParamSnapshots = new ArrayList<SegParam>(Collections.nCopies(numofframes, new SegParam()));
    snakePluginListSnapshots = new ArrayList<SnakePluginList>(
            Collections.nCopies(numofframes, new SnakePluginList(BOA_.NUM_SNAKE_PLUGINS, pf, vu)));
    isFrameEdited = new ArrayList<Boolean>(Collections.nCopies(numofframes, false));
    LOGGER.debug("Initialize storage of size: " + numofframes + " size of segParams: "
            + segParamSnapshots.size());
  }

  /**
   * Make snapshot of current objects state.
   * 
   * @param frame actual frame numbered from 1
   * @see com.github.celldynamics.quimp.SnakePluginList
   */
  public void store(int frame) {
    LOGGER.debug(
            "Data stored at frame:" + frame + " size of segParams is " + segParamSnapshots.size());
    segParamSnapshots.set(frame - 1, new SegParam(segParam));
    // download Plugin config as well
    snakePluginListSnapshots.set(frame - 1, snakePluginList.getDeepCopy());
  }

  /**
   * Restore from snapshots data to current one.
   * 
   * <p>Technically makes reference links between snapshots and fields keeping hot data.
   * 
   * @param frame current frame
   * @see com.github.celldynamics.quimp.SnakePluginList
   */
  public void restore(int frame) {
    LOGGER.trace("Data restored from frame:" + frame);
    SegParam tmp = segParamSnapshots.get(frame - 1);
    if (tmp != null) {
      segParam = tmp;
    }
    snakePluginList = snakePluginListSnapshots.get(frame - 1);
    snakePluginList.uploadPluginsConfig(); // same plugin across frames is represented by the same
    // instance, every time frame is restored plugin configuration must be updated to current
  }

  /**
   * Copy {@link SegParam} data from given snapshot to current frame.
   * 
   * <p>Exchange data between {@link #segParamSnapshots} and {@link #segParam} fields making
   * copy of object but not reference like {@link #restore(int)}.
   * 
   * @param sourceFrame valid index of frame to copy parameters from. Numbered from 1
   * @see BOAState#copyPluginListFromSnapshot(int)
   */
  public void copySegParamFromSnapshot(int sourceFrame) {
    SegParam tmp = segParamSnapshots.get(sourceFrame - 1);
    if (tmp != null) {
      segParam = new SegParam(tmp);
    }
  }

  /**
   * Copy {@link SnakePluginList} data from given snapshot to current frame.
   * 
   * <p>Exchange data between {@link #snakePluginListSnapshots} and {@link #snakePluginList} fields
   * making copy of object but not reference like {@link #restore(int)}.
   * 
   * @param sourceFrame valid index of frame to copy parameters from. Numbered from 1
   * @see #copySegParamFromSnapshot(int)
   */
  public void copyPluginListFromSnapshot(int sourceFrame) {
    snakePluginList.clear(); // clear current
    // snakePluginListSnapshots keeps only params and name, whereas deepCopy requires real
    // instance to download config from it.
    SnakePluginList previous = snakePluginListSnapshots.get(sourceFrame - 1);
    // So create instances on copy first (config will be uploaded here)
    previous.afterSerialize();
    // and make deep copy downloading config back
    snakePluginList = previous.getDeepCopy();
    snakePluginList.afterSerialize(); // instance all plugins
    // qState.store(qState.boap.frame); // copy to current snapshot
    // recalculatePlugins(); // update screen

  }

  /**
   * Store information whether frame was edited only.
   * 
   * <p>Can be called when global state does not change, e.g. user clicked Edit button so
   * parameters and plugins have not been modified.
   * 
   * @param frame current frame numbered from 1
   */
  public void storeOnlyEdited(int frame) {
    isFrameEdited.set(frame - 1, true);
  }

  /**
   * Reset BOAState class.
   * 
   * <p>This method does:
   * <ol>
   * <li>Closes all windows from plugins
   * <li>Cleans all snapshots
   * <li>Set default parameters
   * </ol>
   * 
   * @param ip current image object, can be null. In latter case only subclasses are
   *        initialized
   * @param pf PluginFactory used for creating plugins
   * @param vu ViewUpdater reference
   */
  public void reset(final ImagePlus ip, final PluginFactory pf, final ViewUpdater vu) {
    if (snakePluginList != null) {
      snakePluginList.clear();
    }
    if (snakePluginListSnapshots != null) {
      for (SnakePluginList sp : snakePluginListSnapshots) {
        if (sp != null) {
          sp.clear();
        }
      }
    }
    // boap = new BOAp(); // must be disabled becaue boap keeps some data related to loaded
    // image that never changes
    segParam = new SegParam(); // and SegParam
    initializeSnapshots(ip, pf, vu);
  }

  /**
   * Should be called before serialization.
   * 
   * <p>Creates ArrayLists from Shape.
   */
  @Override
  public void beforeSerialize() {
    nest.beforeSerialize(); // prepare snakes
    if (binarySegmentationPlugin != null) {
      binarySegmentationParam = binarySegmentationPlugin.getPluginConfig();
    } else {
      binarySegmentationParam = new ParamList();
    }

    // snakePluginListSnapshots and segParamSnapshots do not need beforeSerialize()
  }

  /*
   * (non-Javadoc)
   * 
   * @see com.github.celldynamics.quimp.filesystem.IQuimpSerialize#afterSerialize()
   */
  @Override
  public void afterSerialize() throws Exception {
    LOGGER.trace("After serialize called");
    nest.afterSerialize(); // rebuild Shape<T extends PointsList<T>> from ArrayList used for storing
    snakePluginList.afterSerialize(); // assumes that snakePluginList contains valid refs to Facory

    // recreate file objects. without these lines the File objects after serialization are
    // created but they do not keep information about root, e.g. getAbsolutePath() returns
    // program_path/stored_in_json_path
    boap.outputFileCore = new File(boap.outputFileCore.toString());
    boap.orgFile = new File(boap.orgFile.toString());
    // restore local segParam to be first from segParamSnapshots
    if (segParamSnapshots.size() > 0) {
      if (segParamSnapshots.get(0) != null) {
        segParam = new SegParam(segParamSnapshots.get(0));
        // otherwise segParam will be default owng to SegParam constructor
      }
    }
  }

  /**
   * Write set of snake parameters to disk.
   * 
   * <p>writeParams method creates <i>paQP</i> master file, referencing other associated files and
   * <i>csv</i> file with statistics.
   * 
   * <p>Compatibility layer with old QuimP
   * 
   * @param sid ID of cell. If many cells segmented in one time, QuimP produces separate parameter
   *        file for every of them
   * @param startF Start frame (typically beginning of stack)
   * @param endF End frame (typically end of stack)
   * @see com.github.celldynamics.quimp.QParams
   * @see <a href=
   *      "http://www.trac-wsbc.linkpc.net:8080/trac/QuimP/ticket/176#comment:3">ticket/176</a>
   */
  public void writeParams(int sid, int startF, int endF) {
    try {
      if (boap.saveSnake) {
        File paramFile = new File(boap.deductParamFileName(sid));
        QParams qp = new QParams(paramFile);
        qp.setSegImageFile(boap.orgFile);
        qp.setSnakeQP(new File(boap.deductSnakeFileName(sid)));
        qp.setStatsQP(new File(boap.deductStatsFileName(sid)));
        qp.setImageScale(BOA_.qState.boap.imageScale);
        qp.setFrameInterval(BOA_.qState.boap.imageFrameInterval);
        qp.setStartFrame(startF);
        qp.setEndFrame(endF);
        qp.nmax = boap.NMAX;
        qp.setBlowup(segParam.blowup);
        qp.maxIterations = segParam.max_iterations;
        qp.sampleTan = segParam.sample_tan;
        qp.sampleNorm = segParam.sample_norm;
        qp.deltaT = boap.delta_t;
        qp.setNodeRes(segParam.nodeRes);
        qp.velCrit = segParam.vel_crit;
        qp.centralForce = segParam.f_central;
        qp.contractForce = segParam.f_contract;
        qp.imageForce = segParam.f_image;
        qp.frictionForce = boap.f_friction;
        qp.finalShrink = segParam.finalShrink;
        qp.sensitivity = boap.sensitivity;

        qp.writeParams();
      }
    } catch (IOException e) {
      LOGGER.debug(e.getMessage(), e);
      LOGGER.error("Could not write parameters to file", e.getMessage());
    }
  }

  /**
   * Read set of snake parameters from disk.
   * 
   * <p>readParams method reads <i>paQP</i> master file, referencing other associated files.
   * 
   * @param paramFile paQP configuration file
   * 
   * @return Status of operation, true when file has been loaded successfully, false when file has
   *         not been opened correctly or {@link com.github.celldynamics.quimp.QParams#readParams()}
   *         returned false
   * @see com.github.celldynamics.quimp.QParams
   * @see <a href=
   *      "http://www.trac-wsbc.linkpc.net:8080/trac/QuimP/ticket/176#comment:3">ticket/176</a>
   */
  public boolean readParams(File paramFile) {
    boap.readQp = new QParams(paramFile);

    try {
      boap.readQp.readParams();
    } catch (QuimpException e) {
      BOA_.log("Failed to read parameter file " + e.getMessage());
      return false;
    }
    loadParams(boap.readQp);
    BOA_.log("Successfully read parameters");
    return true;
  }

  /**
   * Build internal boa state from QParams object.
   * 
   * <p><b>Warning</b>
   * 
   * <p>frame interval and image scale are not loaded by this function due to compatibility with BOA
   * workflow - user set scale on beginning and then use this method to load paQP files.
   * 
   * @param readQp QParams object
   */
  public void loadParams(QParams readQp) {

    boap.NMAX = readQp.nmax;
    segParam.blowup = readQp.getBlowup();
    segParam.max_iterations = readQp.maxIterations;
    segParam.sample_tan = readQp.sampleTan;
    segParam.sample_norm = readQp.sampleNorm;
    boap.delta_t = readQp.deltaT;
    segParam.nodeRes = readQp.getNodeRes();
    segParam.vel_crit = readQp.velCrit;
    segParam.f_central = readQp.centralForce;
    segParam.f_contract = readQp.contractForce;
    segParam.f_image = readQp.imageForce;

    if (readQp.paramFormat == QParams.QUIMP_11) {
      segParam.finalShrink = readQp.finalShrink;
    }
    boap.readQp = readQp;
    // copy loaded data to snapshots
    for (int f = readQp.getStartFrame(); f <= readQp.getEndFrame(); f++) {
      store(f);
    }
  }
}