Nest.java

package com.github.celldynamics.quimp;

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

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

import com.github.celldynamics.quimp.filesystem.IQuimpSerialize;
import com.github.celldynamics.quimp.geom.ExtendedVector2d;
import com.github.celldynamics.quimp.geom.SegmentedShapeRoi;
import com.github.celldynamics.quimp.utils.QuimPArrayUtils;

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

/**
 * Represent collection of SnakeHandlers.
 * 
 * @author rtyson
 * @author p.baniukiewicz
 */
public class Nest implements IQuimpSerialize {

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

  /**
   * List of SnakeHandlers.
   * 
   * <p>SnakeHandlers get subsequent IDs therefore index in this array matches SnakeHandler's ID.
   * This relation can be broken when any cell is deleted using
   * {@link #removeHandler(SnakeHandler)} method that simply removes it from array shifting other
   * objects down.
   * 
   * <p>This array should be accessed by {@link #getHandler(int)} when one needs access handler on
   * certain index but does not care about its ID or {@link #getHandlerofId(int)} when ID is
   * crucial.
   */
  private ArrayList<SnakeHandler> sHs;
  /**
   * Number of stored snakes in nest.
   */
  private int NSNAKES;
  /**
   * Number of live stored snakes in nest.
   */
  private int ALIVE;
  /**
   * Next free ID.
   */
  private int nextID;

  /**
   * Default constructor.
   */
  public Nest() {
    NSNAKES = 0;
    ALIVE = 0;
    nextID = 0;
    sHs = new ArrayList<SnakeHandler>();
  }

  /**
   * Convert array of SegmentedShapeRoi to SnakeHandlers.
   * 
   * <p>Conversion within one SnakeHandler is stopped when there is defective Snake.
   * 
   * @param roiArray First level stands for objects (SnakeHandlers, second for Snakes within one
   *        chain
   */
  public void addHandlers(ArrayList<ArrayList<SegmentedShapeRoi>> roiArray) {
    LOGGER.trace("Adding " + roiArray.size() + " SnakeHandlers");
    for (List<SegmentedShapeRoi> lsS : roiArray) { // over chains (same cell different frames)
      try {
        sHs.add(new SnakeHandler(lsS, nextID));
        nextID++;
        NSNAKES++;
        ALIVE++;
      } catch (Exception e) {
        LOGGER.error("A snake on frame " + lsS.get(0).getFrame() + " failed to initilise "
                + e.getMessage());
      }
    }
  }

  /**
   * Add rois to Nest - convert them to Snake and store in SnakeHandler.
   * 
   * @param roiArray roiArray
   * @param startFrame start frame, rois from roiArray are added to subsequent frames.
   */
  public void addHandlers(Roi[] roiArray, int startFrame) {
    int i = 0;
    for (; i < roiArray.length; i++) {
      try {
        sHs.add(new SnakeHandler(roiArray[i], startFrame, nextID));
        nextID++;
        NSNAKES++;
        ALIVE++;
      } catch (Exception e) {
        BOA_.log("A snake failed to initilise: " + e.getMessage());
      }
    }
    BOA_.log("Added " + roiArray.length + " cells at frame " + startFrame);
    BOA_.log("Cells being tracked: " + NSNAKES);
  }

  /**
   * Add roi to Nest - convert them to Snake and store in SnakeHandler.
   * 
   * @param r ROI object that contain image object to be segmented
   * @param startFrame Current frame
   * @return SnakeHandler object that is also stored in Nest
   */
  public SnakeHandler addHandler(final Roi r, int startFrame) {
    SnakeHandler sh;
    try {
      sh = new SnakeHandler(r, startFrame, nextID);
      sHs.add(sh);
      nextID++;
      NSNAKES++;
      ALIVE++;
      BOA_.log("Added one cell, begining frame " + startFrame);
    } catch (Exception e) {
      BOA_.log("Added cell failed to initilise");
      LOGGER.debug(e.getMessage(), e);
      return null;
    }
    BOA_.log("Cells being tracked: " + NSNAKES);
    return sh;
  }

  /**
   * Gets SnakeHandler.
   * 
   * @param s Index of SnakeHandler to get.
   * @return SnakeHandler stored on index s. It may refer to SnakeHandler ID but may break when
   *         any cell has been deleted.
   * @see #getHandlerofId(int)
   */
  public SnakeHandler getHandler(int s) {
    return sHs.get(s);
  }

  /**
   * Return list of handlers stored in Nest.
   * 
   * @return unmodifiable list of handlers.
   */
  public List<SnakeHandler> getHandlers() {
    return Collections.unmodifiableList(sHs);
  }

  /**
   * Get SnakeHandler of given id.
   * 
   * @param id ID of SnakeHandler to find in Nest.
   * @return SnakeHandler with demanded ID. Throw exception if not found.
   * @see #getHandler(int)
   */
  public SnakeHandler getHandlerofId(int id) {
    int ret = -1;
    for (int i = 0; i < sHs.size(); i++) {
      if (sHs.get(i) != null && sHs.get(i).getID() == id) {
        ret = i;
        break;
      }
    }
    if (ret < 0) {
      throw new IllegalArgumentException("SnakeHandler of index " + id + " not found in nest");
    } else {
      return getHandler(ret);
    }
  }

  /**
   * Write all Snakes to file.
   * 
   * <p>File names are deducted in called functions.
   * 
   * @return true if write operation has been successful
   * @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 {
    Iterator<SnakeHandler> shitr = sHs.iterator();
    ArrayList<SnakeHandler> toRemove = new ArrayList<>(); // will keep handler to remove
    SnakeHandler sh;
    while (shitr.hasNext()) {
      sh = (SnakeHandler) shitr.next(); // get SnakeHandler from Nest
      sh.findLastFrame(); // find its last frame (frame with valid contour)
      if (sh.getStartFrame() > sh.getEndFrame()) {
        IJ.error("Snake " + sh.getID() + " not written as its empty. Deleting it.");
        toRemove.add(sh);
        continue;
      }
      if (!sh.writeSnakes()) {
        return false;
      }
    }
    // removing from list (after iterator based loop)
    for (int i = 0; i < toRemove.size(); i++) {
      removeHandler(toRemove.get(i));
    }
    return true;
  }

  /**
   * Remove SnakeHandler from Nest.
   * 
   * @param sh handler to remove.
   */
  public void kill(final SnakeHandler sh) {
    sh.kill();
    ALIVE--;
  }

  /**
   * Make all Snakes live in nest.
   */
  public void reviveNest() {
    Iterator<SnakeHandler> shitr = sHs.iterator();
    while (shitr.hasNext()) {
      SnakeHandler sh = (SnakeHandler) shitr.next();
      sh.revive();
    }
    ALIVE = NSNAKES;
  }

  /**
   * Check if Nest is empty.
   * 
   * @return true if Nest is empty
   */
  public boolean isVacant() {
    if (NSNAKES == 0) {
      return true;
    }
    return false;
  }

  /**
   * Check if there is live Snake in Nest.
   * 
   * @return true if all snakes in Nest are dead
   */
  public boolean allDead() {
    if (ALIVE == 0 || NSNAKES == 0) {
      return true;
    }
    return false;
  }

  /**
   * Return true is all snake handlers are froze by user.
   * 
   * @return true if all frozen or no snake handlers.
   */
  public boolean allFrozen() {
    boolean ret = true;
    for (SnakeHandler s : sHs) {
      ret = ret && s.isSnakeHandlerFrozen();
    }
    return ret;
  }

  /**
   * Write <i>stQP</i> file using current Snakes
   * 
   * <p><b>Warning</b>
   * 
   * <p>It can set current slice in ImagePlus (modifies the object state).
   * 
   * @param oi instance of current ImagePlus (required by CellStat that extends
   *        ij.measure.Measurements
   * @param saveStats if true stQP file is saved in disk, false stats are evaluated only and
   *        returned
   * @return CellStat objects with calculated statistics for every cell.
   */
  public List<CellStatsEval> analyse(final ImagePlus oi, boolean saveStats) {
    OutlineHandler outputH;
    SnakeHandler sh;
    ArrayList<CellStatsEval> ret = new ArrayList<>();
    Iterator<SnakeHandler> shitr = sHs.iterator();
    while (shitr.hasNext()) {
      sh = (SnakeHandler) shitr.next();

      File statsFile;
      outputH = new OutlineHandler(sh);
      if (saveStats == true) { // compatibility with old (#263), reread snakes from snQP
        statsFile = new File(BOA_.qState.boap.deductStatsFileName(sh.getID()));
      } else { // new approach store all in QCONF
        statsFile = null;
      }
      CellStatsEval tmp = new CellStatsEval(outputH, oi, statsFile,
              BOA_.qState.boap.getImageScale(), BOA_.qState.boap.getImageFrameInterval());
      ret.add(tmp);
    }
    return ret;
  }

  /**
   * Reset live snakes to ROI's.
   */
  public void resetNest() {
    reviveNest();
    Iterator<SnakeHandler> shitr = sHs.iterator();
    ArrayList<SnakeHandler> toRemove = new ArrayList<>(); // will keep handler to remove
    while (shitr.hasNext()) {
      SnakeHandler sh = (SnakeHandler) shitr.next();
      try {
        sh.reset();
      } catch (Exception e) {
        LOGGER.error("Could not reset snake " + e.getMessage(), e);
        BOA_.log("Could not reset snake " + sh.getID());
        BOA_.log("Removing snake " + sh.getID());
        // collect handler to remove. It will be removed later to avoid list modification in
        // iterator (#186)
        toRemove.add(sh);
      }
    }
    // removing from list (after iterator based loop)
    for (int i = 0; i < toRemove.size(); i++) {
      removeHandler(toRemove.get(i));
    }
  }

  /**
   * Remove {@link SnakeHandler} from Nest.
   * 
   * @param sh SnakeHandler to remove.
   */
  public void removeHandler(final SnakeHandler sh) {
    if (sh.isLive()) {
      ALIVE--;
    }
    sHs.remove(sh);
    NSNAKES--;
  }

  /**
   * Remove all handlers from Nest. Make Nest empty.
   */
  public void cleanNest() {
    sHs.clear();
    NSNAKES = 0;
    ALIVE = 0;
    nextID = 0;
  }

  /**
   * Get NEst size.
   * 
   * @return Get number of SnakeHandlers (snakes) in nest
   */
  public int size() {
    return NSNAKES;
  }

  /**
   * Prepare for segmentation from frame f.
   * 
   * @param f current frame under segmentation
   */
  void resetForFrame(int f) {
    reviveNest();
    Iterator<SnakeHandler> shitr = sHs.iterator();
    ArrayList<SnakeHandler> toRemove = new ArrayList<>(); // will keep handler to remove
    while (shitr.hasNext()) {
      SnakeHandler sh = (SnakeHandler) shitr.next();
      if (sh.isSnakeHandlerFrozen()) {
        continue; // do not update live snake for frozen, it is updated in runBoa
      }
      try {
        if (f <= sh.getStartFrame()) {
          // BOA_.log("Reset snake " + sH.getID() + " as Roi");
          sh.reset();
        } else {
          // BOA_.log("Reset snake " + sH.getID() + " as prev snake");
          sh.resetForFrame(f);
        }
      } catch (Exception e) {
        LOGGER.debug("Could not reset snake " + e.getMessage(), e);
        BOA_.log("Could not reset snake " + sh.getID());
        BOA_.log("Removing snake " + sh.getID());
        // collect handler to remove. It will be removed later to avoid list modification in
        // iterator (#186)
        toRemove.add(sh);
      }
    }
    // removing from list (after iterator based loop)
    for (int i = 0; i < toRemove.size(); i++) {
      removeHandler(toRemove.get(i));
    }
  }

  /**
   * Get list of snakes (its IDs) that are on frame frame.
   * 
   * @param frame Frame find snakes in
   * @return List of Snake id on frame
   */
  public List<Integer> getSnakesforFrame(int frame) {
    ArrayList<Integer> ret = new ArrayList<Integer>();
    Iterator<SnakeHandler> shiter = sHs.iterator();
    while (shiter.hasNext()) { // over whole nest
      SnakeHandler sh = shiter.next(); // for every SnakeHandler
      if (sh.getStartFrame() > frame || sh.getEndFrame() < frame) {
        continue; // no snake in frame
      }
      if (sh.isStoredAt(frame)) { // if limits are ok check if this particular snake exist
        // it is not deleted by user on this particular frame after successful creating as
        // series of Snakes
        Snake s = sh.getStoredSnake(frame);
        ret.add(s.getSnakeID()); // if yes get its id
      }
    }
    return ret;
  }

  /**
   * Count the snakes that exist at, or after, frame.
   * 
   * @param frame frame
   * @return number of snakes
   */
  int nbSnakesAt(int frame) {
    int n = 0;
    for (int i = 0; i < NSNAKES; i++) {
      if (sHs.get(i).getStartFrame() >= frame) {
        n++;
      }
    }
    return n;
  }

  /**
   * Store OutlineHandler as finalSnake.
   * 
   * <p>Use {@link com.github.celldynamics.quimp.SnakeHandler#copyFromFinalToSeg()} to populate
   * snake over segSnakes.
   * 
   * <p>Conversion to Snakes is through Rois
   * 
   * @param oh OutlineHandler to convert from.
   */
  public void addOutlinehandler(final OutlineHandler oh) {
    SnakeHandler sh = addHandler(oh.indexGetOutline(0).asFloatRoi(), oh.getStartFrame());
    if (sh == null) {
      LOGGER.error("Outline handler could not be added");
      return;
    }
    Outline o;
    for (int i = oh.getStartFrame(); i <= oh.getEndFrame(); i++) {
      o = oh.getStoredOutline(i);
      sh.storeRoi((PolygonRoi) o.asFloatRoi(), i);
    }
    sh.copyFromFinalToSeg();
  }

  /**
   * Find Snake that distance of its centroid to given point is smaller than given value.
   * 
   * @param offScreenX x coordinate
   * @param offScreenY y coordinate
   * @param frame frame
   * @param dist maximal distance
   * @return SnakeHandler that meets requirements or null.
   */
  public SnakeHandler findClosestTo(int offScreenX, int offScreenY, int frame, double dist) {
    SnakeHandler snakeH;
    Snake snake;
    ExtendedVector2d snakeV;
    ExtendedVector2d mdV = new ExtendedVector2d(offScreenX, offScreenY);
    List<Double> distance = new ArrayList<Double>();
    List<Integer> index = new ArrayList<>(); // associated with distance, keeps handler index

    for (int i = 0; i < size(); i++) { // calc all distances
      snakeH = getHandler(i);
      if (snakeH.isStoredAt(frame)) {
        snake = snakeH.getStoredSnake(frame);
        snakeV = snake.getCentroid();
        distance.add(ExtendedVector2d.lengthP2P(mdV, snakeV));
        index.add(i);
      }
    }
    int minIndex = QuimPArrayUtils.minListIndex(distance);
    double minDistance = distance.get(minIndex);
    if (minDistance < dist) {
      return getHandler(index.get(minIndex));
    } else {
      return null;
    }
  }

  /*
   * (non-Javadoc)
   * 
   * @see com.github.celldynamics.quimp.filesystem.IQuimpSerialize#beforeSerialize()
   */
  @Override
  public void beforeSerialize() {
    Iterator<SnakeHandler> shitr = sHs.iterator();
    ArrayList<SnakeHandler> toRemove = new ArrayList<>(); // will keep handler to remove
    SnakeHandler sh;
    // sanity operation - delete defective snakes
    while (shitr.hasNext()) {
      sh = (SnakeHandler) shitr.next(); // get SnakeHandler from Nest
      sh.findLastFrame(); // find its last frame (frame with valid contour)
      if (sh.getStartFrame() > sh.getEndFrame()) {
        IJ.error("Snake " + sh.getID() + " not written as its empty. Deleting it.");
        toRemove.add(sh);
        continue;
      }
      sh.beforeSerialize();
    }
    // removing from list (after iterator based loop)
    for (int i = 0; i < toRemove.size(); i++) {
      removeHandler(toRemove.get(i));
    }
  }

  /*
   * (non-Javadoc)
   * 
   * @see com.github.celldynamics.quimp.filesystem.IQuimpSerialize#afterSerialize()
   */
  @Override
  public void afterSerialize() throws Exception {
    Iterator<SnakeHandler> shitr = sHs.iterator();
    SnakeHandler sh;
    while (shitr.hasNext()) {
      sh = (SnakeHandler) shitr.next(); // get SnakeHandler from Nest
      sh.afterSerialize();
    }

  }

  /*
   * (non-Javadoc)
   * 
   * @see java.lang.Object#toString()
   */
  @Override
  public String toString() {
    return "Nest [sHs=" + sHs + ", NSNAKES=" + NSNAKES + ", ALIVE=" + ALIVE + ", nextID=" + nextID
            + "]";
  }
}