QParams.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.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Random;

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

import com.github.celldynamics.quimp.BOAState.BOAp;
import com.github.celldynamics.quimp.filesystem.FileExtensions;
import com.github.celldynamics.quimp.utils.QuimpToolsCollection;

import ij.IJ;

/**
 * Container class for parameters defining the whole process of analysis in QuimP. Stores parameters
 * read from configuration files and provide them to different modules. Supports writing and reading
 * segmentation parameters from files (paQP). This class defines file format used for storing
 * parameters in file. Object of this class is used for creating local configuration objects for
 * ECMM and QAnalysis modules. Process only main paQP file. QuimP uses several files to store
 * segmentation results and algorithm parameters:
 * <ul>
 * <li>.paQP - core file, contains reference to images and parameters of algorithm. This file is
 * saved and processed by QParams class</li>
 * <li>.snQP - contains positions of all nodes for every frame</li>
 * <li>.stQP - basic shape statistics for every frame</li>
 * <li>.mapQP - maps described in documentation</li>
 * </ul>
 * 
 * <p>This class exists for compatibility purposes. Allows reading old files. There is also child
 * class
 * QParamsEsxhanger that is based on new file format. Because QParams is strongly integrated with
 * QuimP it has been left.
 * 
 * @author rtyson
 * @see BOAp
 * 
 */
public class QParams {

  /**
   * The Constant LOGGER.
   */
  static final Logger LOGGER = LoggerFactory.getLogger(QParams.class.getName());

  /**
   * The Constant OLD_QUIMP. Denotes version of QuimP < Q11
   */
  public static final int OLD_QUIMP = 1;

  /**
   * The Constant QUIMP_11. Denotes version Q11
   */
  public static final int QUIMP_11 = 2;

  /**
   * The Constant NEW_QUIMP. Denotes version > Q11
   */
  public static final int NEW_QUIMP = 3;
  /**
   * Name of the case. Used to set <i>fileName</i> and <i>path</i>
   */
  private File paramFile;
  /**
   * Indicates format of data file.
   */
  protected int paramFormat;
  /**
   * Name of the data file - without path and extension. Equals to name of the case. If
   * initialised from {@link QParamsQconf} contains underscored cell number as well.
   * 
   * @see com.github.celldynamics.quimp.BOAState.BOAp
   */
  private String fileName;
  /**
   * Path where user files exist.
   * 
   * @see com.github.celldynamics.quimp.BOAState.BOAp
   */
  private String path;
  private File segImageFile;
  private File snakeQP;

  /**
   * The stats QP.
   */
  protected File statsQP;
  /**
   * This field is set by direct call from ANA. Left here for compatibility reasons. Main holder
   * of fluTiffs is {@link com.github.celldynamics.quimp.plugin.ana.ANAp}
   */
  public File[] fluTiffs;

  private File convexFile;
  private File coordFile;
  private File motilityFile;
  private File originFile;
  private File xmapFile;
  private File ymapFile;
  private File[] fluFiles;

  private double imageScale;
  private double frameInterval;
  private int startFrame;
  private int endFrame;

  private int blowup;
  private double nodeRes;

  int nmax;
  int maxIterations;
  int sampleTan;
  int sampleNorm;

  double deltaT;
  double velCrit;
  double centralForce;
  double contractForce;
  double imageForce;
  double frictionForce;

  /**
   * Shrink value.
   */
  public double finalShrink;
  /**
   * The cortex width.
   */
  public double cortexWidth;

  /**
   * The key.
   */
  long key;

  /**
   * The sensitivity.
   */
  double sensitivity; // no longer used. blank holder
  /**
   * Indicate if <i>snQP</i> has been processed by ECMM (<tt>true</tt>). Set by checkECMMrun.
   */
  private boolean ecmmHasRun = false;

  /**
   * Check if ECMM was run.
   * 
   * @return the ecmmHasRun
   */
  public boolean isEcmmHasRun() {
    return ecmmHasRun;
  }

  /**
   * Instantiates a new q params.
   */
  public QParams() {

  }

  /**
   * Read basic information from <i>paQP</i> file such as its name and path. Initialise structures
   * 
   * @param p <i>paQP</i> file, should contain underscored cell number and absolute path.
   */
  public QParams(File p) {
    setParamFile(p);

    paramFormat = QParams.QUIMP_11;

    segImageFile = new File("/");
    snakeQP = new File("/");
    statsQP = new File("/");

    fluTiffs = new File[3];
    fluTiffs[0] = new File("/");
    fluTiffs[1] = new File("/");
    fluTiffs[2] = new File("/");

    imageScale = -1;
    frameInterval = -1;
    startFrame = -1;
    endFrame = -1;
    nmax = -1;
    blowup = -1;
    maxIterations = -1;
    sampleTan = -1;
    sampleNorm = -1;
    deltaT = -1;
    nodeRes = -1;
    velCrit = -1;
    centralForce = -1;
    contractForce = -1;
    imageForce = -1;
    frictionForce = -1;
    finalShrink = -1;
    cortexWidth = 0.7;
    key = -1;
    sensitivity = -1;
  }

  /**
   * Get name of parameter file with extension (paQP).
   * 
   * @return the paramFile
   * @see #getPath()
   */
  public File getParamFile() {
    return paramFile;
  }

  /**
   * Set name of parameter file.
   * 
   * @param paramFile the paramFile to set. Extension will be removed.
   */
  public void setParamFile(File paramFile) {
    fileName = QuimpToolsCollection.removeExtension(paramFile.getName());
    this.paramFile = paramFile;
    path = paramFile.getParent();
    // path is used for producing other file names in directory where paramFile. This is in case
    // if paramFile does not contain path (should not happen)
    if (path == null) {
      throw new IllegalArgumentException("Input file: " + paramFile
              + " must contain path as other files will be created in its folder");
    }
  }

  /**
   * Get the name of parameter file but without extension.
   * 
   * @return the prefix. This name probably contains also underscored cell number. It is added
   *         when object of this is created from {@link QParamsQconf} and should be added when
   *         creating only this object QParams(File).
   */
  public String getFileName() {
    return fileName;
  }

  /**
   * getPath.
   * 
   * @return the path of param file (no file name)
   */
  public String getPath() {
    return path;
  }

  /**
   * getPathasPath.
   * 
   * @return the path of param file as Path (no file name)
   * @see #getParamFile()
   */
  public Path getPathasPath() {
    return Paths.get(this.getPath());
  }

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

  /**
   * setNodeRes.
   * 
   * @param nodeRes the nodeRes to set
   */
  public void setNodeRes(double nodeRes) {
    this.nodeRes = nodeRes;
  }

  /**
   * getBlowup.
   * 
   * @return the blowup
   */
  public int getBlowup() {
    return blowup;
  }

  /**
   * setBlowup.
   * 
   * @param blowup the blowup to set
   */
  public void setBlowup(int blowup) {
    this.blowup = blowup;
  }

  /**
   * getImageScale.
   * 
   * @return the imageScale
   */
  public double getImageScale() {
    return imageScale;
  }

  /**
   * setImageScale.
   * 
   * @param imageScale the imageScale to set
   */
  public void setImageScale(double imageScale) {
    this.imageScale = imageScale;
  }

  /**
   * getFrameInterval.
   * 
   * @return the frameInterval
   */
  public double getFrameInterval() {
    return frameInterval;
  }

  /**
   * setFrameInterval.
   * 
   * @param frameInterval the frameInterval to set
   */
  public void setFrameInterval(double frameInterval) {
    this.frameInterval = frameInterval;
  }

  /**
   * getStartFrame.
   * 
   * @return the startFrame
   */
  public int getStartFrame() {
    return startFrame;
  }

  /**
   * setStartFrame.
   * 
   * @param startFrame the startFrame to set
   */
  public void setStartFrame(int startFrame) {
    this.startFrame = startFrame;
  }

  /**
   * getEndFrame.
   * 
   * @return the endFrame
   */
  public int getEndFrame() {
    return endFrame;
  }

  /**
   * setEndFrame.
   * 
   * @param endFrame the endFrame to set
   */
  public void setEndFrame(int endFrame) {
    this.endFrame = endFrame;
  }

  /**
   * Get snQP file name.
   * 
   * @return the snakeQP
   */
  public File getSnakeQP() {
    return snakeQP;
  }

  /**
   * Set snQP file name.
   * 
   * @param snakeQP the snakeQP to set
   */
  public void setSnakeQP(File snakeQP) {
    this.snakeQP = snakeQP;
  }

  /**
   * Get stats file name.
   * 
   * @return the statsQP
   */
  public File getStatsQP() {
    return statsQP;
  }

  /**
   * Get names of flu files.
   * 
   * @return the fluFiles
   */
  public File[] getFluFiles() {
    return fluFiles;
  }

  /**
   * Set stats file name.
   * 
   * @param statsQP the statsQP to set
   */
  public void setStatsQP(File statsQP) {
    this.statsQP = statsQP;
  }

  /**
   * Get name of BOA loaded image.
   * 
   * @return the segImageFile
   */
  public File getSegImageFile() {
    return segImageFile;
  }

  /**
   * Set name of BOA loaded image.
   * 
   * @param segImageFile the segImageFile to set
   */
  public void setSegImageFile(File segImageFile) {
    this.segImageFile = segImageFile;
  }

  /**
   * getConvexFile name.
   * 
   * @return the convexFile
   */
  public File getConvexFile() {
    return convexFile;
  }

  /**
   * setConvexFile name.
   * 
   * @param convexFile the convexFile to set
   */
  public void setConvexFile(File convexFile) {
    this.convexFile = convexFile;
  }

  /**
   * getCoordFile name.
   * 
   * @return the coordFile
   */
  public File getCoordFile() {
    return coordFile;
  }

  /**
   * setCoordFile name.
   * 
   * @param coordFile the coordFile to set
   */
  public void setCoordFile(File coordFile) {
    this.coordFile = coordFile;
  }

  /**
   * getMotilityFile name.
   * 
   * @return the motilityFile
   */
  public File getMotilityFile() {
    return motilityFile;
  }

  /**
   * setMotilityFile name.
   * 
   * @param motilityFile the motilityFile to set
   */
  public void setMotilityFile(File motilityFile) {
    this.motilityFile = motilityFile;
  }

  /**
   * getOriginFile name.
   * 
   * @return the originFile
   */
  public File getOriginFile() {
    return originFile;
  }

  /**
   * setOriginFile name.
   * 
   * @param originFile the originFile to set
   */
  public void setOriginFile(File originFile) {
    this.originFile = originFile;
  }

  /**
   * Get x coord map file.
   * 
   * @return the xmapFile
   */
  public File getxmapFile() {
    return xmapFile;
  }

  /**
   * Set x coord map file.
   * 
   * @param xmapFile the xmapFile to set
   */
  public void setxmapFile(File xmapFile) {
    this.xmapFile = xmapFile;
  }

  /**
   * Get y coord map file.
   * 
   * @return the ymapFile
   */
  public File getymapFile() {
    return ymapFile;
  }

  /**
   * Set y coord map file.
   * 
   * @param ymapFile the ymapFile to set
   */
  public void setymapFile(File ymapFile) {
    this.ymapFile = ymapFile;
  }

  /**
   * Return loaded format.
   * 
   * @return the paramFormat {@value #OLD_QUIMP}, {@value #NEW_QUIMP}, {@link #QUIMP_11}
   */
  public int getParamFormat() {
    return paramFormat;
  }

  /**
   * Read the paQP file specified by paramFile
   * 
   * <p>See {@link #QParams(File)}. Create handles to files stored as names in <i>paQP</i>. Read
   * segmentation parameters.
   * 
   * @throws QuimpException on wrong file that can not be interpreted or read.
   * @throws IllegalStateException if input file is empty
   */
  public void readParams() throws QuimpException {
    paramFormat = QParams.OLD_QUIMP;
    BufferedReader d = null;
    try {
      d = new BufferedReader(new FileReader(paramFile));

      String l = d.readLine();
      if (l == null) {
        throw new IllegalStateException(
                "File " + paramFile.getAbsolutePath() + " seems to be empty");
      }
      if (!(l.length() < 2)) {
        String fileID = l.substring(0, 2);
        if (!fileID.equals("#p")) {
          throw new QuimpException("Not compatible paramater file");
        }
      } else {
        throw new QuimpException("Not compatible paramater file");
      }
      key = (long) QuimpToolsCollection.s2d(d.readLine()); // key
      segImageFile = new File(d.readLine()); // image file name

      String sn = d.readLine();
      // fileName = sn;
      if (!l.substring(0, 3).equals("#p2")) { // old format, fix file names
        sn = sn.substring(1); // strip the dot off snQP file name
        // fileName = fileName.substring(1); // strip the dot off file
        // name
        int lastDot = sn.lastIndexOf(".");

        String tempS = sn.substring(0, lastDot);
        statsQP = new File(paramFile.getParent() + tempS + FileExtensions.statsFileExt);
      }
      snakeQP = new File(paramFile.getParent() + "" + sn); // snQP file
      LOGGER.debug("snake file: " + snakeQP.getAbsolutePath());

      d.readLine(); // # blank line
      imageScale = QuimpToolsCollection.s2d(d.readLine());
      if (imageScale == 0) {
        IJ.log("Warning. Image scale was zero. Set to 1");
        imageScale = 1;
      }
      frameInterval = QuimpToolsCollection.s2d(d.readLine());

      d.readLine(); // skip #segmentation parameters
      nmax = (int) QuimpToolsCollection.s2d(d.readLine());
      deltaT = QuimpToolsCollection.s2d(d.readLine());
      maxIterations = (int) QuimpToolsCollection.s2d(d.readLine());
      nodeRes = QuimpToolsCollection.s2d(d.readLine());
      blowup = (int) QuimpToolsCollection.s2d(d.readLine());
      sampleTan = (int) QuimpToolsCollection.s2d(d.readLine());
      sampleNorm = (int) QuimpToolsCollection.s2d(d.readLine());
      velCrit = QuimpToolsCollection.s2d(d.readLine());

      centralForce = QuimpToolsCollection.s2d(d.readLine());
      contractForce = QuimpToolsCollection.s2d(d.readLine());
      frictionForce = QuimpToolsCollection.s2d(d.readLine());
      imageForce = QuimpToolsCollection.s2d(d.readLine());
      sensitivity = QuimpToolsCollection.s2d(d.readLine());

      if (l.substring(0, 3).equals("#p2")) { // new format
        paramFormat = QParams.QUIMP_11;
        // new params
        // # - new parameters (cortext width, start frame, end frame, final shrink, statsQP,
        // fluImage)
        d.readLine();
        cortexWidth = QuimpToolsCollection.s2d(d.readLine());
        startFrame = (int) QuimpToolsCollection.s2d(d.readLine());
        endFrame = (int) QuimpToolsCollection.s2d(d.readLine());
        finalShrink = QuimpToolsCollection.s2d(d.readLine());
        statsQP = new File(paramFile.getParent() + "" + d.readLine());

        d.readLine(); // # fluo channel tiffs
        fluTiffs[0] = new File(d.readLine());
        fluTiffs[1] = new File(d.readLine());
        fluTiffs[2] = new File(d.readLine());
      }
      this.guessOtherFileNames(); // generate handles of other files that will be created here
      // check if snQP file is already processed by ECMM. Set ecmmHasRun
      ecmmHasRun = verifyEcmminpsnQP();
      if (ecmmHasRun) {
        LOGGER.info("ECMM has been run on this paFile data");
      }
    } catch (IOException e) {
      throw new QuimpException(e);
    } finally {
      try {
        if (d != null) {
          d.close();
        }
      } catch (IOException e) {
        LOGGER.error("Can not close file " + e);
      }
    }
  }

  /**
   * Write params.
   *
   * @throws IOException Signals that an I/O exception has occurred.
   */
  public void writeParams() throws IOException {
    LOGGER.debug("Write " + FileExtensions.configFileExt + " at: " + paramFile);
    if (paramFile.exists()) {
      paramFile.delete();
    }

    Random generator = new Random();
    double d = generator.nextDouble() * 1000000; // 6 digit key to ID job
    key = Math.round(d);

    PrintWriter ppW = new PrintWriter(new FileWriter(paramFile), true);

    ppW.print("#p2 - QuimP parameter file (QuimP11). Created " + QuimpToolsCollection.dateAsString()
            + "\n");
    ppW.print(IJ.d2s(key, 0) + "\n");
    ppW.print(segImageFile.getAbsolutePath() + "\n");
    ppW.print(File.separator + snakeQP.getName() + "\n");
    // pPW.print(outFile.getAbsolutePath() + "\n");

    ppW.print("#Image calibration (scale, frame interval)\n");
    ppW.print(IJ.d2s(imageScale, 6) + "\n");
    ppW.print(IJ.d2s(frameInterval, 3) + "\n");

    // according to BOAState and /trac/QuimP/wiki/QuimpQp
    ppW.print("#segmentation parameters (" + "Maximum number of nodes, " + "ND, "
            + "Max iterations, " + "Node spacing, " + "Blowup, " + "Sample tan, " + "Sample norm, "
            + "Crit velocity, " + "Central F, " + "Contract F, " + "ND, " + "Image force, "
            + "ND)\n");
    ppW.print(IJ.d2s(nmax, 0) + "\n");
    ppW.print(IJ.d2s(deltaT, 6) + "\n");
    ppW.print(IJ.d2s(maxIterations, 6) + "\n");
    ppW.print(IJ.d2s(nodeRes, 6) + "\n");
    ppW.print(IJ.d2s(blowup, 6) + "\n");
    ppW.print(IJ.d2s(sampleTan, 0) + "\n");
    ppW.print(IJ.d2s(sampleNorm, 0) + "\n");
    ppW.print(IJ.d2s(velCrit, 6) + "\n");
    ppW.print(IJ.d2s(centralForce, 6) + "\n");
    ppW.print(IJ.d2s(contractForce, 6) + "\n");
    ppW.print(IJ.d2s(frictionForce, 6) + "\n");
    ppW.print(IJ.d2s(imageForce, 6) + "\n");
    ppW.print(IJ.d2s(sensitivity, 6) + "\n");

    ppW.print("# - new parameters (cortex width, start frame, end frame,"
            + " final shrink, statsQP, fluImage)\n");
    ppW.print(IJ.d2s(cortexWidth, 2) + "\n");
    ppW.print(IJ.d2s(startFrame, 0) + "\n");
    ppW.print(IJ.d2s(endFrame, 0) + "\n");
    ppW.print(IJ.d2s(finalShrink, 2) + "\n");
    ppW.print(File.separator + statsQP.getName() + "\n");

    ppW.print("# - Fluorescence channel tiff's\n");
    ppW.print(fluTiffs[0].getAbsolutePath() + "\n");
    ppW.print(fluTiffs[1].getAbsolutePath() + "\n");
    ppW.print(fluTiffs[2].getAbsolutePath() + "\n");

    ppW.print("#END");

    ppW.close();
  }

  /**
   * Traverse through current directory looking for <i>paQP</i> files.
   * 
   * <p><b>Remarks</b>
   * 
   * <p>Current <i>paQP</i> file (that passed to QParams(File)) is not counted.
   * 
   * @return Array of file handlers or empty array if there is no <i>paQP</i> files (except
   *         paramFile)
   */
  public File[] findParamFiles() {
    File[] otherPaFiles;
    File directory = new File(path);
    ArrayList<String> paFiles = new ArrayList<String>();

    if (directory.isDirectory()) {
      String[] filenames = directory.list();
      if (filenames == null) {
        otherPaFiles = new File[0];
        return otherPaFiles;
      }
      String extension;

      for (int i = 0; i < filenames.length; i++) {
        if (filenames[i].matches("\\.") || filenames[i].matches("\\.\\.")
                || filenames[i].matches(paramFile.getName())) {
          continue;
        }
        extension = QuimpToolsCollection.getFileExtension(filenames[i]);
        if (extension.matches(FileExtensions.configFileExt.substring(1))) {
          paFiles.add(filenames[i]);
          LOGGER.info("paFile: " + filenames[i]);
        }
      }
    }
    if (paFiles.isEmpty()) {
      otherPaFiles = new File[0];
      return otherPaFiles;
    } else {
      otherPaFiles = new File[paFiles.size()];
      for (int j = 0; j < otherPaFiles.length; j++) {
        otherPaFiles[j] =
                new File(directory.getAbsolutePath() + File.separator + (String) paFiles.get(j));
      }
      return otherPaFiles;
    }
  }

  /**
   * Generate names and handles of files associated with paQP that will be created in result of
   * analysis.
   */
  protected void guessOtherFileNames() {
    LOGGER.debug("prefix: " + fileName);

    convexFile = new File(path + File.separator + fileName + FileExtensions.convmapFileExt);

    coordFile = new File(path + File.separator + fileName + FileExtensions.coordmapFileExt);
    motilityFile = new File(path + File.separator + fileName + FileExtensions.motmapFileExt);
    originFile = new File(path + File.separator + fileName + FileExtensions.originmapFileExt);
    xmapFile = new File(path + File.separator + fileName + FileExtensions.xmapFileExt);
    ymapFile = new File(path + File.separator + fileName + FileExtensions.ymapFileExt);

    fluFiles = new File[3];
    fluFiles[0] = new File(
            path + File.separator + fileName + FileExtensions.fluomapFileExt.replace('%', '1'));
    fluFiles[1] = new File(
            path + File.separator + fileName + FileExtensions.fluomapFileExt.replace('%', '2'));
    fluFiles[2] = new File(
            path + File.separator + fileName + FileExtensions.fluomapFileExt.replace('%', '3'));

  }

  /**
   * Verify if ECMM was run on snQP file.
   * 
   * <p>snQP file contains ECMM string in first line after ECMM.
   * 
   * @return true if ECMM was run
   * @throws IOException on error
   */
  public boolean verifyEcmminpsnQP() throws IOException {
    BufferedReader br = new BufferedReader(new FileReader(snakeQP));
    String line = br.readLine();
    if (line == null) {
      br.close();
      throw new IllegalStateException("File " + snakeQP + " seems to be empty");
    }
    br.close();
    return line.contains("-ECMM");
  }

}