SnakeHandler.java

package com.github.celldynamics.quimp;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Arrays;
import java.util.List;

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

import com.github.celldynamics.quimp.BOA_.CustomStackWindow;
import com.github.celldynamics.quimp.filesystem.FileExtensions;
import com.github.celldynamics.quimp.filesystem.IQuimpSerialize;
import com.github.celldynamics.quimp.geom.SegmentedShapeRoi;
import com.github.celldynamics.quimp.plugin.utils.QuimpDataConverter;
import com.github.celldynamics.quimp.utils.QuimpToolsCollection;

import ij.IJ;
import ij.gui.PolygonRoi;
import ij.gui.Roi;

/**
 * Store all the snakes computed for one cell across frames and it is responsible for writing them
 * to file.
 * 
 * <p>For any further processing outside QuimP <tt>finalSnakes</tt> should be used.
 * 
 * @author rtyson
 * @author p.baniukiewicz
 *
 */
public class SnakeHandler extends ShapeHandler<Snake> implements IQuimpSerialize {

  /**
   * The Constant LOGGER.
   */
  static final Logger LOGGER = LoggerFactory.getLogger(SnakeHandler.class.getName());
  /**
   * initial ROI, not stored but rebuilt from snake on load.
   */
  private transient Roi roi;
  /**
   * initial snake being currently processed.
   * 
   * <p>Live snake is preserved for whole {@link SnakeHandler} regardless image frame.
   */
  private Snake liveSnake;
  /**
   * Series of snakes, result of cell segm. and plugin processing.
   * 
   * <p>These are the same as stored in <i>snQP</i> file for each frame and can be plugin processed.
   */
  private Snake[] finalSnakes;
  /**
   * series of snakes, result of cell segmentation only. Before plugin application.
   */
  private Snake[] segSnakes;
  /**
   * ID of Snakes stored in this SnakeHandler.
   */
  private int ID;
  /**
   * If true this snakeHandler (and related to it continuous series of snakes) is not modified
   * segmentation is called.
   * 
   * @see BOA_#runBoa(int, int)
   * @see #freezeHandler()
   * @see #unfreezeHandler()
   */
  private boolean snakeHandlerFrozen = false;

  /**
   * Instantiates a new snake handler. Do not initialise anything.
   */
  public SnakeHandler() {
  }

  /**
   * Constructor of SnakeHandler. Stores ROI with object for segmentation.
   * 
   * @param r ROI with selected object
   * @param frame Current frame for which the ROI is taken
   * @param id Unique Snake ID controlled by Nest object
   * @throws BoaException on problem with Snake creation
   */
  public SnakeHandler(final Roi r, int frame, int id) throws BoaException {
    this();
    startFrame = frame;
    endFrame = BOA_.qState.boap.getFrames();
    roi = r;
    // snakes array keeps snakes across frames from current to end. Current
    // is that one for which cell has been added
    finalSnakes = new Snake[BOA_.qState.boap.getFrames() - startFrame + 1]; // stored snakes
    segSnakes = new Snake[BOA_.qState.boap.getFrames() - startFrame + 1]; // stored snakes
    ID = id;
    liveSnake = new Snake(r, ID, false);
    backupLiveSnake(frame);
  }

  /**
   * Copy constructor. Create SnakeHandler from list of already prepared outlines.
   * 
   * <p>For every frame it copies provided snake to all three arrays: finalSnakes, segSnakes,
   * liveSnake and sets first and last frame using data from SegmentedShapeRoi object
   * 
   * @param snakes List of outlines that will be propagated from first frame. First frame is wrote
   *        down in first element of this list
   * @param id Unique Snake ID controlled by Nest object
   * @throws BoaException on problem with Snake creation
   * @see com.github.celldynamics.quimp.geom.SegmentedShapeRoi
   */
  public SnakeHandler(List<SegmentedShapeRoi> snakes, int id) throws BoaException {
    this();
    startFrame = snakes.get(0).getFrame(); // get first frame from outline
    finalSnakes = new Snake[BOA_.qState.boap.getFrames() - startFrame + 1]; // stored snakes
    segSnakes = new Snake[BOA_.qState.boap.getFrames() - startFrame + 1]; // stored snakes
    ID = id;
    roi = snakes.get(0); // set initial roi to first snake
    for (SegmentedShapeRoi ss : snakes) {
      liveSnake = new Snake(ss.getOutlineasPoints(), ID); // tmp for next two methods
      backupLiveSnake(ss.getFrame()); // fill segSnakes for frame
      storeLiveSnake(ss.getFrame()); // fill finalSnakes for frame
    }
    endFrame = snakes.get(snakes.size() - 1).getFrame();
    liveSnake = new Snake(snakes.get(0).getOutlineasPoints(), ID); // set live again for frame
    // SegmentedShapeRoi contains number of frame that it came from. The are sorted as frames so
    // last originates from last frame
    endFrame = snakes.get(snakes.size() - 1).getFrame();
    LOGGER.debug("Added" + this.toString()); // try toString
  }

  /**
   * Make copy of liveSnake into final snakes array.
   * 
   * @param frame Frame for which liveSnake will be copied to
   */
  public void storeLiveSnake(int frame) {
    finalSnakes[frame - startFrame] = null; // delete at current frame
    finalSnakes[frame - startFrame] = new Snake(liveSnake, ID);
  }

  /**
   * Stores liveSnake (currently processed) in segSnakes array.
   * 
   * <p>For one SnakeHandler there is only one liveSnake which is processed "in place" by
   * segmentation methods. It is virtually moved from frame to frame and copied to final snakes
   * after segmentation on current frame and processing by plugins. It must be backed up for every
   * frame to make possible restoring original snakes when active plugin has been deselected.
   * 
   * @param frame current frame
   */
  public void backupLiveSnake(int frame) {
    LOGGER.trace("Stored live snake in frame " + frame + " ID " + ID);
    segSnakes[frame - startFrame] = null; // delete at current frame
    segSnakes[frame - startFrame] = new Snake(liveSnake, ID);
  }

  /**
   * Makes copy of snake and store it as final snake.
   * 
   * @param snake Snake to store
   * @param frame Frame for which liveSnake will be copied to
   */
  public void storeThisSnake(final Snake snake, int frame) {
    finalSnakes[frame - startFrame] = null; // delete at current frame
    finalSnakes[frame - startFrame] = new Snake(snake, ID);
  }

  /**
   * Makes copy of snake and store it as segmented snake.
   * 
   * @param snake Snake to store
   * @param frame Frame for which liveSnake will be copied to
   */
  public void backupThisSnake(final Snake snake, int frame) {
    segSnakes[frame - startFrame] = null; // delete at current frame
    segSnakes[frame - startFrame] = new Snake(snake, ID);
  }

  /**
   * Copy all segSnakes to finalSnakes.
   */
  public void copyFromSegToFinal() {
    for (int i = 0; i < segSnakes.length; i++) {
      if (segSnakes[i] == null) {
        finalSnakes[i] = null;
      } else {
        finalSnakes[i] = new Snake(segSnakes[i]);
      }
    }
  }

  /**
   * Copy all finalSnakes to segSnakes.
   */
  public void copyFromFinalToSeg() {
    for (int i = 0; i < finalSnakes.length; i++) {
      if (finalSnakes[i] == null) {
        segSnakes[i] = null;
      } else {
        segSnakes[i] = new Snake(finalSnakes[i]);
      }
    }
  }

  /**
   * Copy final snake from frame to liveSnake.
   * 
   * @param frame frame to copy from (counted from 1)
   */
  public void copyFromFinalToLive(int frame) {
    if (finalSnakes[frame - startFrame] == null) {
      return;
    }
    liveSnake = new Snake(finalSnakes[frame - startFrame]);

  }

  /**
   * Write Snakes from this handler to *.snPQ file. Display also user interface
   * 
   * @return true if save has been successful or false if user cancelled it
   * @throws IOException when the file exists but is a directory rather than a regular file, does
   *         not exist but cannot be created, or cannot be opened for any other reason
   */
  public boolean writeSnakes() throws IOException {
    String snakeOutFile = BOA_.qState.boap.deductSnakeFileName(ID);
    LOGGER.debug("Write " + FileExtensions.snakeFileExt + " at: " + snakeOutFile);
    PrintWriter pw = new PrintWriter(new FileWriter(snakeOutFile), true); // auto flush
    pw.write("#QuimP11 Node data");
    pw.write("\n#Node Position\tX-coord\tY-coord\tOrigin\tG-Origin\tSpeed");
    pw.write("\tFluor_Ch1\tCh1_x\tCh1_y\tFluor_Ch2\tCh2_x\tCh2_y\tFluor_CH3\tCH3_x\tCh3_y\n#");

    Snake s;
    for (int i = startFrame; i <= endFrame; i++) {
      s = getStoredSnake(i);
      s.setPositions(); // calculate position field
      pw.write("\n#Frame " + i);
      write(pw, i + 1, s.getNumPoints(), s.getHead());
    }
    pw.close();
    BOA_.qState.writeParams(ID, startFrame, endFrame);

    if (BOA_.qState.boap.oldFormat) {
      writeOldFormats();
    }
    return true;
  }

  /**
   * Write one Node to disk (one line in snPQ file).
   * 
   * @param pw print writer
   * @param frame frame number
   * @param nodes number of nodes
   * @param n node to write
   */
  private void write(final PrintWriter pw, int frame, int nodes, Node n) {
    pw.print("\n" + nodes);

    do {
      // fluo values (x,y, itensity)
      pw.print("\n" + IJ.d2s(n.position, 6) + "\t" + IJ.d2s(n.getX(), 2) + "\t"
              + IJ.d2s(n.getY(), 2) + "\t0\t0\t0" + "\t-2\t-2\t-2\t-2\t-2\t-2\t-2\t-2\t-2");
      n = n.getNext();
    } while (!n.isHead());

  }

  /**
   * Format before QuimP11.
   * 
   * @throws IOException on file problem
   */
  private void writeOldFormats() throws IOException {
    // create file to outpurt old format
    File old = new File(BOA_.qState.boap.getOutputFileCore().getParent(),
            BOA_.qState.boap.getFileName() + ".dat");
    PrintWriter pw = new PrintWriter(new FileWriter(old), true); // auto flush

    for (int i = 0; i < finalSnakes.length; i++) {
      if (finalSnakes[i] == null) {
        break;
      }
      if (i != 0) {
        pw.print("\n");
      } // no new line at top
      pw.print(finalSnakes[i].getNumPoints());

      Node n = finalSnakes[i].getHead();
      do {
        pw.print("\n" + IJ.d2s(n.getX(), 6));
        pw.print("\n" + IJ.d2s(n.getY(), 6));
        n = n.getNext();
      } while (!n.isHead());
    }
    pw.close();

    old = new File(BOA_.qState.boap.getOutputFileCore().getParent(),
            BOA_.qState.boap.getFileName() + ".dat_tn");
    pw = new PrintWriter(new FileWriter(old), true); // auto flush

    for (int i = 0; i < finalSnakes.length; i++) {
      if (finalSnakes[i] == null) {
        break;
      }
      if (i != 0) {
        pw.print("\n");
      } // no new line at top
      pw.print(finalSnakes[i].getNumPoints());

      Node n = finalSnakes[i].getHead();
      do {
        pw.print("\n" + IJ.d2s(n.getX(), 6));
        pw.print("\n" + IJ.d2s(n.getY(), 6));
        pw.print("\n" + n.getTrackNum());
        n = n.getNext();
      } while (!n.isHead());
    }
    pw.close();

    old = new File(BOA_.qState.boap.getOutputFileCore().getParent(),
            BOA_.qState.boap.getFileName() + ".dat1");
    pw = new PrintWriter(new FileWriter(old), true); // auto flush

    pw.print(IJ.d2s(BOA_.qState.boap.NMAX, 6) + "\n");
    pw.print(IJ.d2s(BOA_.qState.boap.delta_t, 6) + "\n");
    pw.print(IJ.d2s(BOA_.qState.segParam.max_iterations, 6) + "\n");
    pw.print(IJ.d2s(BOA_.qState.segParam.getMin_dist(), 6) + "\n");
    pw.print(IJ.d2s(BOA_.qState.segParam.getMax_dist(), 6) + "\n");
    pw.print(IJ.d2s(BOA_.qState.segParam.blowup, 6) + "\n");
    pw.print(IJ.d2s(BOA_.qState.segParam.sample_tan, 6) + "\n");
    pw.print(IJ.d2s(BOA_.qState.segParam.sample_norm, 6) + "\n");
    pw.print(IJ.d2s(BOA_.qState.segParam.vel_crit, 6) + "\n");
    pw.print(IJ.d2s(BOA_.qState.segParam.f_central, 6) + "\n");
    pw.print(IJ.d2s(BOA_.qState.segParam.f_contract, 6) + "\n");
    pw.print(IJ.d2s(BOA_.qState.boap.f_friction, 6) + "\n");
    pw.print(IJ.d2s(BOA_.qState.segParam.f_image, 6) + "\n");
    pw.print(IJ.d2s(1.0, 6) + "\n");
    pw.print(IJ.d2s(BOA_.qState.boap.sensitivity, 6) + "\n");
    pw.print(IJ.d2s(BOA_.qState.boap.cut_every, 6) + "\n");
    pw.print("100");

    pw.close();
  }

  /**
   * Gets the live snake.
   *
   * @return the live snake
   */
  public Snake getLiveSnake() {
    return liveSnake;
  }

  /**
   * Gets the backup snake.
   *
   * @param f the f
   * @return the backup snake
   */
  public Snake getBackupSnake(int f) {
    LOGGER.trace("Asked for backup snake at frame " + f + " ID " + ID);
    if (f - startFrame < 0) {
      LOGGER.info("Tried to access negative frame store: frame: " + f + ", snakeID: " + ID);
      return null;
    }
    return segSnakes[f - startFrame];
  }

  /**
   * Return final Snake (after plugins) stored for frame f.
   * 
   * @param f frame
   * @return Snake at frame f or null
   */
  public Snake getStoredSnake(int f) {
    if (f - startFrame < 0) {
      LOGGER.info("Tried to access negative frame store: frame: " + f + ", snakeID: " + ID);
      return null;
    }
    return finalSnakes[f - startFrame];
  }

  /**
   * Validate whether there is any Snake at frame f.
   * 
   * @param f frame to validate
   * @return true if finalSnakes array contains valid Snake at frame f
   */
  boolean isStoredAt(int f) {
    if (f - startFrame < 0) {
      return false;
    } else if (f - startFrame >= finalSnakes.length) {
      return false;
    } else if (finalSnakes[f - startFrame] == null) {
      return false;
    } else {
      return true;
    }

  }

  /**
   * Read Snake from file.
   *
   * <p>May not be compatible wit old version due to changes in Snake constructor.
   *
   * @param inFile file to read
   * @return value of 1
   * @throws Exception on problem
   * @see <a href="link">com.github.celldynamics.quimp.OutlineHandler.readOutlines(File)</a>
   */
  @Deprecated
  public int snakeReader(final File inFile) throws Exception {
    String thisLine;
    int nn;
    int index;
    double x;
    double y;
    Node head;
    Node n;
    Node prevn;
    int s = 0;
    BufferedReader br = null;

    try {
      br = new BufferedReader(new FileReader(inFile));

      while ((thisLine = br.readLine()) != null) {
        index = 0;
        head = new Node(index); // dummy head node
        head.setHead(true);
        prevn = head;
        index++;

        nn = (int) QuimpToolsCollection.s2d(thisLine);

        for (int i = 0; i < nn; i++) {
          x = QuimpToolsCollection.s2d(br.readLine());
          y = QuimpToolsCollection.s2d(br.readLine());

          n = new Node(index);
          n.setX(x);
          n.setY(y);
          index++;

          prevn.setNext(n);
          n.setPrev(prevn);

          prevn = n;

        }
        // link tail to head
        prevn.setNext(head);
        head.setPrev(prevn);

        finalSnakes[s] = new Snake(head, nn + 1, ID); // dont forget the head
        // due to compatibility with code above. old versions made copies of list WARN potential
        // uncompatibility with old code. old constructor made copy of this list and deleted first
        // dummy node. Now it just covers this list
        finalSnakes[s].removeNode(head);
        s++;
      } // end while
    } catch (IOException e) {
      System.err.println("Error: " + e);
    } finally {
      if (br != null) {
        try {
          br.close();
        } catch (Exception e) {
          e.printStackTrace();
        }
      }
    }

    return 1;
  }

  /**
   * Revive.
   */
  public void revive() {
    liveSnake.alive = true;
  }

  /**
   * Kill.
   */
  public void kill() {
    liveSnake.alive = false;
  }

  /**
   * Reset snakes in handler. Recreate them using stored ROI.
   *
   * @throws BoaException on snake creation problem
   */
  public void reset() throws BoaException {
    liveSnake = new Snake(roi, ID, false);
  }

  /**
   * Gets the id of handler.
   *
   * @return the id
   */
  public int getID() {
    return ID;
  }

  /**
   * Checks if is live snake is live.
   *
   * @return true, if is live
   */
  public boolean isLive() {
    return liveSnake.alive;
  }

  /**
   * Delete snake stored at at frame.
   *
   * @param frame the frame to delete snake from
   */
  void deleteStoreAt(int frame) {
    if (frame - startFrame < 0) {
      BOA_.log("Tried to delete negative frame store\n\tframe:" + frame + "\n\tsnakeID:" + ID);
    } else {
      finalSnakes[frame - startFrame] = null;
      segSnakes[frame - startFrame] = null;
    }
  }

  /**
   * Delete snakes from frame to end.
   *
   * @param frame the start frame to delete from
   */
  void deleteStoreFrom(int frame) {
    for (int i = frame; i <= BOA_.qState.boap.getFrames(); i++) {
      deleteStoreAt(i);
    }
    endFrame = frame;
  }

  /**
   * Prepare current frame for segmentation.
   * 
   * <p>Create liveSnake using final snake stored in previous frame or use original ROI for
   * creating new Snake
   * 
   * @param f Current segmented frame
   */
  void resetForFrame(int f) {
    try {
      if (BOA_.qState.segParam.use_previous_snake) {
        // set to last segmentation ready for blowup
        liveSnake = new Snake((PolygonRoi) this.getStoredSnake(f - 1).asFloatRoi(), ID);
      } else {
        liveSnake = new Snake(roi, ID, false);
      }
    } catch (Exception e) {
      BOA_.log("Could not reset live snake form frame" + f);
      LOGGER.debug(e.getMessage(), e);
    }
  }

  /**
   * Store ROI as snake in finalSnakes.
   * 
   * @param r roi to create Snake from
   * @param frame frame
   */
  void storeRoi(final PolygonRoi r, int frame) {
    try {
      Snake snake = new Snake(r, ID);
      snake.calcCentroid();
      this.deleteStoreAt(frame);
      storeThisSnake(snake, frame);
      backupThisSnake(snake, frame);
      // BOA_.log("Storing ROI snake " + ID + " frame " + f);
    } catch (Exception e) {
      BOA_.log("Could not store ROI");
      LOGGER.debug(e.getMessage(), e);
    }
  }

  /**
   * Find the first missing contour at series of frames and set end frame to the previous one.
   */
  void findLastFrame() {
    for (int i = startFrame; i <= BOA_.qState.boap.getFrames(); i++) {
      if (!isStoredAt(i)) {
        endFrame = i - 1;
        return;
      }
    }
    endFrame = BOA_.qState.boap.getFrames();
  }

  /**
   * Return true if this handler is frozen.
   * 
   * <p>Frozen handler is excluded from frame segmentation.
   * 
   * @return status of this handler.
   * @see #freezeHandler()
   * @see #unfreezeHandler()
   */
  public boolean isSnakeHandlerFrozen() {
    return snakeHandlerFrozen;
  }

  /**
   * Prevent this handler from segmentation.
   * 
   * @see #unfreezeHandler()
   * @see #isSnakeHandlerFrozen()
   * @see CustomStackWindow#itemStateChanged(java.awt.event.ItemEvent) (zoom action)
   */
  public void freezeHandler() {
    snakeHandlerFrozen = true;
  }

  /**
   * Unlock handler.
   * 
   * @see #freezeHandler()
   * @see #isSnakeHandlerFrozen()
   * @see CustomStackWindow#itemStateChanged(java.awt.event.ItemEvent) (zoom action)
   */
  public void unfreezeHandler() {
    snakeHandlerFrozen = false;
  }

  /*
   * (non-Javadoc)
   * 
   * @see java.lang.Object#toString()
   */
  @Override
  public String toString() {
    return "SnakeHandler [liveSnake=" + liveSnake + ", finalSnakes=" + Arrays.toString(finalSnakes)
            + ", ID=" + ID + ", startFrame=" + startFrame + ", endFrame=" + endFrame + "]";
  }

  /**
   * Prepare all Snake stored in this SnakeHandler for saving.
   */
  @Override
  public void beforeSerialize() {
    if (liveSnake != null) {
      liveSnake.beforeSerialize(); // convert liveSnake to array
    }
    for (Snake s : finalSnakes) {
      if (s != null) {
        s.beforeSerialize(); // convert finalSnakes to array
      }
    }
    for (Snake s : segSnakes) {
      if (s != null) {
        s.beforeSerialize(); // convert segSnakes to array
      }
    }
    findLastFrame(); // set correct first-last frame field
  }

  /**
   * Prepare all Snake stored in this SnakeHandler for loading.
   */
  @Override
  public void afterSerialize() throws Exception {
    if (liveSnake != null) {
      liveSnake.afterSerialize();
    }
    for (Snake s : finalSnakes) {
      if (s != null) {
        s.afterSerialize();
      }
    }
    for (Snake s : segSnakes) {
      if (s != null) {
        s.afterSerialize();
      }
    }
    // restore roi as first snake from segmented snakes
    if (segSnakes.length > 0) {
      int i = 0;
      while (i < segSnakes.length && segSnakes[i++] == null) {
        ; // find first not null snake
      }
      QuimpDataConverter dc = new QuimpDataConverter(segSnakes[--i]);
      // rebuild roi from snake
      roi = new PolygonRoi(dc.getFloatX(), dc.getFloatY(), Roi.FREEROI);
    }
  }
}