FormatConverter.java

package com.github.celldynamics.quimp.filesystem.converter;

import java.awt.Frame;
import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import javax.swing.JOptionPane;

import org.slf4j.LoggerFactory;

import com.github.celldynamics.quimp.BOAState;
import com.github.celldynamics.quimp.BOA_;
import com.github.celldynamics.quimp.CellStats;
import com.github.celldynamics.quimp.FrameStatistics;
import com.github.celldynamics.quimp.Nest;
import com.github.celldynamics.quimp.Node;
import com.github.celldynamics.quimp.Outline;
import com.github.celldynamics.quimp.OutlineHandler;
import com.github.celldynamics.quimp.PointsList;
import com.github.celldynamics.quimp.QParams;
import com.github.celldynamics.quimp.QParamsQconf;
import com.github.celldynamics.quimp.QuimP;
import com.github.celldynamics.quimp.QuimpException;
import com.github.celldynamics.quimp.Serializer;
import com.github.celldynamics.quimp.Shape;
import com.github.celldynamics.quimp.Snake;
import com.github.celldynamics.quimp.SnakeHandler;
import com.github.celldynamics.quimp.Vert;
import com.github.celldynamics.quimp.filesystem.ANAParamCollection;
import com.github.celldynamics.quimp.filesystem.DataContainer;
import com.github.celldynamics.quimp.filesystem.FileExtensions;
import com.github.celldynamics.quimp.filesystem.OutlinesCollection;
import com.github.celldynamics.quimp.filesystem.QconfLoader;
import com.github.celldynamics.quimp.filesystem.StatsCollection;
import com.github.celldynamics.quimp.geom.ExtendedVector2d;
import com.github.celldynamics.quimp.plugin.ana.ANAp;
import com.github.celldynamics.quimp.plugin.ana.ChannelStat;
import com.github.celldynamics.quimp.plugin.qanalysis.FluoMap;
import com.github.celldynamics.quimp.plugin.qanalysis.Qp;
import com.github.celldynamics.quimp.plugin.qanalysis.STmap;
import com.github.celldynamics.quimp.utils.CsvWritter;
import com.github.celldynamics.quimp.utils.QuimPArrayUtils;

import ch.qos.logback.classic.Logger;

/**
 * This class allows for converting between paQP and QCONF and vice versa.
 * 
 * <p>The following conversion are supported:<br>
 * <b>paQP->QCONF</b><br>
 * [+] paQP->QCONF<br>
 * [+] snQP->QCONF<br>
 * [+] maQP->QCONF<br>
 * [+] stQP->QCONF<br>
 * <b>QCONF->paQP</b><br>
 * [+] QCONF->paQP<br>
 * [+] QCONF->snQP<br>
 * [+] QCONF->maQP<br>
 * [+] QCONF->stQP<br>
 * [-] QCONF->tiffs<br>
 * 
 * <p>This class can be also used to extract data from QCONF {@link DataContainer} and save them as
 * plain csv files.
 * 
 * <p><b>Note</b>
 * 
 * <p>Images are generated regardless used file format in QuimP Q module.
 * 
 * <p>This method is related to fields that are non-transient and any change in serialised classes
 * should be reflected here.
 * 
 * <p>Due to randomness during creating Snakes or Outlines (head node is picked randomly on
 * {@link Shape#removePoint(PointsList, boolean)} these objects stored in QCONF may differ from snQP
 * representation. There are the following rules ({@link DataContainer}):
 * <ol>
 * <li>paQP--QCONF conversion</li>
 * <ol>
 * <li>nest:liveSnake ({@link SnakeHandler#getLiveSnake()}) is filled but <b>should not be
 * used</b></li>
 * <li>nest:finalSnake ({@link SnakeHandler#getStoredSnake(int)}) is filled and it is converted from
 * outlines (which are read from snQP file).
 * Starting node may be different than in snQP due to conversion between Snakes and Outlines.
 * <li>ECMMState:outlines ({@link DataContainer#getEcmmState()}) contains data read from
 * snQP file if ECMM has been run (-ECMM string in snQP file header).
 * <li>ANA (@link {@link DataContainer#getANAState()}) is created only if ECMM data is available.
 * <li>Stats {@link DataContainer#getStats()} - read from stQP.csv file if present. If not present
 * empty structure is created in QCONF but that file should be considered as defective.
 * </ol>
 * <li>QCONF--paQP</li>
 * <ol>
 * <li>Snakes in snQP are in the same order like:
 * <ol>
 * <li>nest:finalSnakes ({@link SnakeHandler#getStoredSnake(int)}) if there is no ECCM data
 * (position is used)
 * <li>ECMMState:outlines ({@link DataContainer#getEcmmState()}) if there is ECMM data (coord is
 * used)
 * </ol>
 * </ol>
 * </ol>
 * 
 * <p>Generally converter expects correct structure of paQP experiment, e.g. if there are many
 * cells (_0.paQP, _1.paQP, etc), all cases should have other modules run on them with the same
 * parameters (e.g. map resolution). There should be the same set of files for each case.
 * 
 * <p>See {@link #doConversion()} for supported files.
 * 
 * @author p.baniukiewicz
 *
 */
public class FormatConverter {
  protected static Logger logger =
          (Logger) LoggerFactory.getLogger(FormatConverter.class.getName());
  private QconfLoader qcL;
  private Path path; // path of file extracted from qcL
  private Path filename; // file name extracted from qcL, no extension

  /**
   * Order of parameters saved by {@link #saveOutline(Outline, CsvWritter)}.
   */
  public static final String[] headerEcmmOutline = {
      //!> order of data, must follow writeLine below
      "charge",
      "distance",
      "fluo-ch1_x",
      "fluo-ch1_y",
      "fluo-ch1_i",
      "fluo-ch2_x",
      "fluo-ch2_y",
      "fluo-ch2_i",
      "fluo-ch3_x", 
      "fluo-ch3_y", 
      "fluo-ch3_i",
      "curvLoc",
      "curvSmooth",
      "curvSum",
      "coord",
      "gLandCoord",
      "node_x",
      "node_y",
      "normal_x",
      "normal_y",
      "tan_x",
      "tan_y",
      "position",
      "frozen"        
      };
  //!<

  /**
   * Do nothing.
   * 
   * @see #attachFile(File)
   */
  public FormatConverter() {
  }

  /**
   * Construct FormatConverter from provided file.
   * 
   * @param fileToConvert file to convert
   * @throws QuimpException if input file can not be loaded
   */
  public FormatConverter(File fileToConvert) throws QuimpException {
    this();
    attachFile(fileToConvert);
  }

  /**
   * Attach file for conversion.
   * 
   * <p>Do the same job as {@link #FormatConverter(File)} but can be used if
   * {@link #FormatConverter()}
   * was used.
   * 
   * @param fileToConvert file to convert
   * @throws QuimpException if input file can not be loaded
   */
  public void attachFile(File fileToConvert) throws QuimpException {
    logger.info("Converting file: " + fileToConvert.getName());
    qcL = new QconfLoader(fileToConvert);
    path = Paths.get(fileToConvert.getParent());
    filename = Paths.get(qcL.getQp().getFileName()); // can contain xx_0 if old file loaded
  }

  /**
   * Construct conversion object from QconfLoader.
   * 
   * @param qcL reference to QconfLoader
   */
  public FormatConverter(QconfLoader qcL) {
    logger.debug("Use provided QconfLoader");
    this.qcL = qcL;
    this.path = qcL.getQp().getPathasPath();
    this.filename = Paths.get(qcL.getQp().getFileName()); // can contain xx_0 if old file loaded
  }

  /**
   * Show message with conversion capabilities.
   * 
   * @param frame parent frame
   */
  public void showConversionCapabilities(Frame frame) {
    //!>
    JOptionPane.showMessageDialog(frame,
                "Supported conversions\n"
                + "paQP->QCONF features:\n"
                + " [+] paQP->QCONF\n"
                + " [+] snQP->QCONF\n"
                + " [+] maQP->QCONF\n"
                + " [+] stQP->QCONF\n"
                + "QCONF->paQP features:\n"
                + " [+] QCONF->paQP\n"
                + " [+] QCONF->snQP\n"
                + " [+] QCONF->maQP\n"
                + " [+] QCONF->stQP\n"
                + " [-] QCONF->tiffs",
                "Warning",
                JOptionPane.WARNING_MESSAGE);
    //!<
  }

  /**
   * Build QCONF from old datafile provided in constructor.
   * 
   * <p>Input file given in constructor is considered as starting one. paQP files in successive
   * numbers are searched in the same directory. The internal <tt>qcL</tt> variable will be
   * overrode on this method call.
   * 
   * @throws QuimpException on wrong inputs
   * @throws IOException on saving QCONF
   */
  private void generateNewDataFiles() throws QuimpException, IOException {
    if (qcL.isFileLoaded() == QParams.NEW_QUIMP) {
      throw new IllegalArgumentException("Can not convert from new format to new");
    }
    boolean ecmmRun = false;
    logger.info("File is in old format, new format will be created");
    // create storage
    DataContainer dt = new DataContainer();
    dt.BOAState = new BOAState(qcL.getImage());
    dt.ECMMState = null;
    dt.BOAState.nest = new Nest();
    // dT.ANAState = new ANAStates();
    ArrayList<STmap> maps = new ArrayList<>(); // temporary - we do not know number of cells

    // extract paQP number from file name (loaded in constructor)
    int last = filename.toString().lastIndexOf('_'); // position of _ before number
    if (last < 0) {
      throw new QuimpException(
              "Input file name must be in format name_XX.paQP, where XX is cell number.");
    }

    // check which file number user selected. End program if user made mistake
    try {
      int numofpaqp; // number extracted from paQP name
      numofpaqp = Integer
              .parseInt(filename.toString().substring(last + 1, filename.toString().length()));
      if (numofpaqp != 0) { // warn user if not first file selected
        throw new QuimpException("Selected paQP file is not first (not a _0.paQP file).");
      }
    } catch (NumberFormatException e) {
      throw new QuimpException("Number can not be found in file paQP name. "
              + "Check if file name is in format name_XX.paQP, where XX is cell number.");
    }
    // cut last number from file name name
    String orginal = filename.toString().substring(0, last);

    int i; // PaQP files counter
    // run conversion for all paQP files. conversion always starts from 0
    i = 0;
    File filetoload = new File(""); // store name_XX.paQP file in loop below
    OutlineHandler oh;
    STmap stMap;
    ANAParamCollection anaP = new ANAParamCollection(); // holder for ANA config, for every cell
    try {
      do {
        // paQP files with _xx number in name
        filetoload =
                Paths.get(qcL.getQp().getPath(), orginal + "_" + i + FileExtensions.configFileExt)
                        .toFile();
        logger.info("Attempting to process " + filetoload.getName());
        if (!filetoload.exists()) { // if does not exist - end loop
          logger.info("File " + filetoload.toString() + " does not exist. Finishing conversion.");
          break;
        }
        // optimisation - first file is already loaded, skip it
        if (i != 0) {
          qcL = new QconfLoader(filetoload); // re-load it
        } else {
          // assume that BOA params are taken from file 0_.paQP
          dt.BOAState.loadParams(qcL.getQp()); // load parameters only once (but not frameinterval,)
          dt.BOAState.boap.setImageFrameInterval(qcL.getQp().getFrameInterval());
          dt.BOAState.boap.setImageScale(qcL.getQp().getImageScale());
        }
        logger.info(toString());
        // initialize snakes (from snQP files)
        logger.info("... Reading snakes");
        // check if ECMM was run on this snake file
        ecmmRun = qcL.getQp().verifyEcmminpsnQP();
        BOA_.qState = dt.BOAState; // for compatibility - create static
        oh = new OutlineHandler(qcL.getQp()); // restore OutlineHandler
        if (ecmmRun) { // create structure if ecmm was run (assume that all paQP was then processed)
          if (dt.ECMMState == null) { // do not overwrite for many paQP
            dt.ECMMState = new OutlinesCollection();
          }
          dt.ECMMState.oHs.add(oh); // store in ECMM object
        } else {
          logger.info("... Reading snakes - no ECMM data");
        }
        dt.BOAState.nest.addOutlinehandler(oh); // covert ECMM to Snake and store in BOA section

        // load maps and store in QCONF
        stMap = new STmap();
        int readMap = 0; // check if we loaded at least one we expected 5 remaining as well
        try {
          logger.info("\tReading " + qcL.getQp().getMotilityFile().getName());
          stMap.setMotMap(QuimPArrayUtils.file2Array(",", qcL.getQp().getMotilityFile()));
          readMap++;
        } catch (IOException e) {
          logger.info(e.getMessage());
          logger.debug(e.getMessage(), e);
        }
        try {
          logger.info("\tReading " + qcL.getQp().getConvexFile().getName());
          stMap.setConvMap(QuimPArrayUtils.file2Array(",", qcL.getQp().getConvexFile()));
          readMap++;
        } catch (IOException e) {
          logger.info(e.getMessage());
          logger.debug(e.getMessage(), e);
        }
        try {
          logger.info("\tReading " + qcL.getQp().getCoordFile().getName());
          stMap.setCoordMap(QuimPArrayUtils.file2Array(",", qcL.getQp().getCoordFile()));
          readMap++;
        } catch (IOException e) {
          logger.info(e.getMessage());
          logger.debug(e.getMessage(), e);
        }
        try {
          logger.info("\tReading " + qcL.getQp().getOriginFile().getName());
          stMap.setOriginMap(QuimPArrayUtils.file2Array(",", qcL.getQp().getOriginFile()));
          readMap++;
        } catch (IOException e) {
          logger.debug(e.getMessage(), e);
          logger.info(e.getMessage());
        }
        try {
          logger.info("\tReading " + qcL.getQp().getxmapFile().getName());
          stMap.setxMap(QuimPArrayUtils.file2Array(",", qcL.getQp().getxmapFile()));
          readMap++;
        } catch (IOException e) {
          logger.info(e.getMessage());
          logger.debug(e.getMessage(), e);
        }
        try {
          logger.info("\tReading " + qcL.getQp().getymapFile().getName());
          stMap.setyMap(QuimPArrayUtils.file2Array(",", qcL.getQp().getymapFile()));
          readMap++;
        } catch (IOException e) {
          logger.info(e.getMessage());
          logger.debug(e.getMessage(), e);
        }
        // number of read maps check (expecting all read if there is at leas one)
        if (readMap >= 1 && readMap < 6) {
          logger.warn("It seems that you have missing maps. Perhaps your dataset is incomplete. "
                  + "This may lead to invalid QCONF file.");
        }
        // Fluoromap
        // first check if there is any FluMap
        int channel = 1; // channel counter for fluoromaps
        int p = 0; // sizes of flumap
        int t = 0; // sizes of flumap
        for (File ff : qcL.getQp().getFluFiles()) { // iterate over filenames
          // if any exist, get its size. Because if there is no maps at all we set this object to
          // null but if there is at least one we have to set all other maps to -1 array of size of
          // that available one
          if (ff.exists()) {
            double[][] tmpFluMap = QuimPArrayUtils.file2Array(",", ff);
            t = tmpFluMap.length;
            p = tmpFluMap[0].length;
            // Fluoromap exist, so other must do as they are generated together, check this
            if (stMap.getT() == 0) { // no map loaded above (fluoro and other are generated togethe)
              logger.warn("It seems that you have fluorosence map but other maps are missing."
                      + " Perhaps your dataset is incomplete. "
                      + "This may lead to invalid QCONF file.");
            }
            break; // assume without checking that all maps are the same
          }
        }
        // if p,t are non zero we know that there is at least one map
        // try to read 3 channels for current paQP
        for (File ff : qcL.getQp().getFluFiles()) {
          // create Fluoro data holder
          FluoMap chm = new FluoMap(t, p, channel);
          if (ff.exists()) { // read file stored in paQP for channel
            logger.info("\tReading " + ff.getName());
            chm.setMap(QuimPArrayUtils.file2Array(",", ff)); // it sets it enabled
          } else {
            chm.setEnabled(false); // not existing, disable
            logger.info("File " + ff.toString() + " not found.");
          }
          stMap.fluoMaps[channel - 1] = chm;
          channel++;
        }
        // add container if there is at least one map
        if (stMap.getT() != 0 || t != 0) {
          maps.add(stMap);
        }
        // ANAState - add ANAp references for every processed paQP, set only non-transient fields
        logger.info("\tFilling ANA");
        ANAp anapTmp = new ANAp();
        anapTmp.scale = qcL.getQp().getImageScale(); // set scale used by setCortextWidthScale
        anapTmp.setCortextWidthScale(qcL.getQp().cortexWidth); // sets also cortexWidthPixel
        anapTmp.fluTiffs = qcL.getQp().fluTiffs; // set files
        anaP.aS.add(anapTmp); // store in ANAParamCollection

        i++; // go to next paQP
      } while (true); // exception thrown by QconfLoader will stop this loop, e.g. trying to load
      // nonexiting file

      // process stQP - all files
      logger.info("\tReading stats");

      StatFileParser obj = new StatFileParser(Paths.get(qcL.getQp().getPath(), orginal).toString());
      List<Path> statFiles = obj.getAllFiles(); // count stQP files
      // and do simple checking
      if (i != statFiles.size()) { // i counted from 0
        logger.warn("It seems that number of stQP files is different than number of paQP files."
                + " Perhaps your dataset is incomplete. This may lead to invalid QCONF file.");
      }
      ArrayList<CellStats> stats = obj.importStQp();
      dt.Stats = new StatsCollection();
      dt.Stats.setStatCollection(stats);

    } catch (Exception e) { // repack exception with proper message about defective file
      throw new QuimpException(
              "File " + filetoload.toString() + " can not be processed: " + e.getMessage(), e);
    }
    // do simple map checking - find if all (none) paQP had (had not) maps.
    int count = 0;
    for (STmap tmp : maps) {
      if (tmp.getT() == 0) {
        count++;
      } else {
        count--;
      }
    }
    if (Math.abs(count) != maps.size()) {
      logger.warn("It seems that some paQP cases have missing maps."
              + " Perhaps your dataset is incomplete. This may lead to invalid QCONF file.");
    }
    // save DataContainer using Serializer
    if (!maps.isEmpty()) {
      dt.QState = maps.toArray(new STmap[0]); // convert to array
    } else {
      dt.QState = null;
    }
    if (ecmmRun) {
      dt.ANAState = anaP;
    } else {
      dt.ANAState = null;
    }

    Serializer<DataContainer> n;
    n = new Serializer<>(dt, QuimP.TOOL_VERSION);
    n.setPretty();
    n.save(path + File.separator + orginal + FileExtensions.newConfigFileExt);
    n = null;
  }

  /**
   * Recreate paQP, snQP, stQP and maQP files from QCONF.
   * 
   * <p>Files are created in directory where QCONF is located.
   * 
   * @throws IOException on file saving error
   * @throws QuimpException if requested data are not available in QCONF
   * 
   */
  private void generateOldDataFiles() throws IOException, QuimpException {
    if (qcL.isFileLoaded() == QParams.QUIMP_11) {
      throw new IllegalArgumentException("Can not convert from old format to old");
    }
    logger.info("File is in new format, old format will be created");
    logger.info(toString());
    DataContainer dt = ((QParamsQconf) qcL.getQp()).getLoadedDataContainer();
    if (dt.getEcmmState() == null) {
      logger.warn("ECMM analysis is not present in QCONF");
      generatepaQP(); // no ecmm data write snakes only
    } else {
      generatesnQP(); // write ecmm data
    }
    if (qcL.isStatsPresent()) {
      saveStats();
    } else {
      logger.warn("Statistics are not present in QCONF");
    }
    if (qcL.isQPresent()) {
      saveMaps(STmap.ALLMAPS);
    } else {
      logger.warn("Q analysis is not present in QCONF");
    }
  }

  /**
   * Save selected maps to files. Support only new file format. Throws
   * {@link IllegalArgumentException} when run from object constructed from old format.
   * 
   * <p>Follow naming convention: <i>ROOT_N_MAPNAME.maQP</i>, where <tt>ROOT</tt> is corename of
   * QCONF file,(<i>ROOT.QCONF</i>), <tt>N</tt> is cell number and <tt>MAPNAME</tt> follows
   * supported map extensions.
   * 
   * @param maps map defined in {@link STmap}
   * @throws QuimpException if maps are not available
   */
  public void saveMaps(int maps) throws QuimpException {
    if (qcL.isFileLoaded() == QParams.QUIMP_11) {
      throw new IllegalArgumentException("New format required.");
    }
    int activeHandler = 0;
    // replace location to location of QCONF
    DataContainer dt = ((QParamsQconf) qcL.getQp()).getLoadedDataContainer();
    dt.BOAState.boap.setOutputFileCore(path + File.separator + filename.toString());
    String name = STmap.LOGGER.getName();
    STmap.LOGGER = logger; // FIXME replace
    Qp params = new Qp();
    try {
      for (STmap stmap : qcL.getQ()) {
        ((QParamsQconf) qcL.getQp()).setActiveHandler(activeHandler++);
        stmap.setParams(params);
        params.setup(qcL.getQp());
        stmap.saveMaps(maps);
      }
    } finally {
      STmap.LOGGER = LoggerFactory.getLogger(name);
    }
  }

  /**
   * Save each snake centroid for each frame.
   * 
   * <p>Produce files /path/core_cellNo_boacentroid.csv
   * 
   * @throws QuimpException if BOA structure is not available
   */
  public void saveBoaCentroids() throws QuimpException {
    if (qcL.isFileLoaded() == QParams.QUIMP_11) {
      throw new IllegalArgumentException("New format required.");
    }
    int activeHandler = 0;
    CsvWritter csv = null;
    for (SnakeHandler sh : qcL.getBOA().nest.getHandlers()) {
      try {
        csv = new CsvWritter(getFeatureFileName("boacentroid", activeHandler, ".csv"), "#frame",
                "centroid_x", "centroid_y");
        logger.info("\tSaved Boa centroids at: " + csv.getPath().getFileName());
        int sf = sh.getStartFrame();
        int ef = sh.getEndFrame();
        for (int f = sf; f <= ef; f++) {
          Snake snake = sh.getStoredSnake(f);
          ExtendedVector2d centroid = snake.getCentroid();
          csv.writeLine((double) f, centroid.x, centroid.y);
        }
      } catch (IOException e) {
        logger.error("Can not write file");
      } finally {
        activeHandler++;
        if (csv != null) {
          csv.close();
        }
      }
    }

  }

  /**
   * Save all data associated with BOA analysis.
   * 
   * <p>Produce files /path/core_cellNo_snake-frame_no.csv with snake data for each frame separately
   * or files /path/core_cellNo_snake.csv with all data in one file (for same snake).
   * 
   * <p>Output file contain only parameters directly included in QCONF in contrary to e.g. snQP
   * files (or results of QCONF->paQP conversion) that contain some extra data calculated.
   * 
   * @param separateFiles Control whether files should be broken into separate files for each frame
   *        (true) and for each object or store all frames in one file (false).
   * @throws QuimpException if BOA structure is not available
   */
  public void saveBoaSnakes(boolean separateFiles) throws QuimpException {
    if (qcL.isFileLoaded() == QParams.QUIMP_11) {
      throw new IllegalArgumentException("New format required.");
    }
    //!> order of data, must follow writeLine below
    final String[] params = {
        "vel_x",
        "vel_y",
        "F-total_x",
        "F-total_y",
        "node_x",
        "node_y",
        "normal_x",
        "normal_y",
        "tan_x",
        "tan_y",
        "position",
        "frozen"};
    //!<
    CsvWritter csv = null;
    int activeHandler = 0;
    for (SnakeHandler sh : qcL.getBOA().nest.getHandlers()) {
      int sf = sh.getStartFrame();
      int ef = sh.getEndFrame();
      try {
        for (int f = sf; f <= ef; f++) {
          Snake snake = sh.getStoredSnake(f);
          if (separateFiles == true) { // create for each frame
            csv = new CsvWritter(getFeatureFileName("snake-frame" + f, activeHandler, ".csv"),
                    params);
          } else if (f == sf) { // create only once on first frame
            csv = new CsvWritter(getFeatureFileName("snake", activeHandler, ".csv"), params);

          }
          if (separateFiles == false) {
            csv.writeLine("#frame " + f); // just add break if one file outputed
          }
          logger.info("\tSaved snakes at: " + csv.getPath().getFileName());
          Iterator<Node> it = snake.iterator();
          while (it.hasNext()) {
            Node n = it.next();
            //!>
            csv.writeLine(
                    n.getVel().x,
                    n.getVel().y,
                    n.getF_total().x,
                    n.getF_total().y,
                    n.getPoint().x,
                    n.getPoint().y,
                    n.getNormal().x,
                    n.getNormal().y,
                    n.getTangent().x,
                    n.getTangent().y,
                    n.getPosition(),
                    n.isFrozen() ? 1.0 : 0.0);
            //!<
          }
          if (separateFiles == true) {
            csv.close(); // after frame
          }
        }
      } catch (IOException e) {
        logger.error("Can not write file");
      } finally {
        activeHandler++;
        if (csv != null) {
          csv.close();
        }
      }
    }
  }

  /**
   * Save all data associated with ECMM and ANA analysis.
   * 
   * <p>Produce files /path/core_cellNo_outline-frame_no.csv with snake data for each frame
   * separately or files /path/core_cellNo_outline.csv with all data in one file (for same snake).
   * 
   * <p>Output file contain only parameters directly included in QCONF in contrary to e.g. snQP
   * files (or results of QCONF->paQP conversion) that contain some extra data calculated.
   * 
   * @param separateFiles Control whether files should be broken into separate files for each frame
   *        (true) and for each object or store all frames in one file (false).
   * @throws QuimpException if ECMM has not been run
   */
  public void saveEcmmOutlines(boolean separateFiles) throws QuimpException {
    if (qcL.isFileLoaded() == QParams.QUIMP_11) {
      throw new IllegalArgumentException("New format required.");
    }
    CsvWritter csv = null;
    int activeHandler = 0;
    for (OutlineHandler oh : qcL.getEcmm().oHs) {
      int sf = oh.getStartFrame();
      int ef = oh.getEndFrame();
      try {
        for (int f = sf; f <= ef; f++) {
          Outline outline = oh.getStoredOutline(f);
          if (separateFiles == true) { // create for each frame
            csv = new CsvWritter(getFeatureFileName("outline-frame" + f, activeHandler, ".csv"),
                    headerEcmmOutline);
          } else if (f == sf) { // create only once on first frame
            csv = new CsvWritter(getFeatureFileName("outline", activeHandler, ".csv"),
                    headerEcmmOutline);
          }
          if (separateFiles == false) {
            csv.writeLine("#frame " + f); // just add break if one file outputed
          }
          saveOutline(outline, csv);
          if (separateFiles == true) {
            csv.close(); // after frame
          }
        }
      } catch (IOException e) {
        logger.error("Can not write file");
      } finally {
        activeHandler++;
        if (csv != null) {
          csv.close();
        }
      }
    }
  }

  /**
   * Save specified outline to {@link CsvWritter}.
   * 
   * @param outline outline to save
   * @param csv opened csv object
   */
  public static void saveOutline(Outline outline, CsvWritter csv) {
    logger.info("\tSaved outlines at: " + csv.getPath().getFileName());
    Iterator<Vert> it = outline.iterator();
    while (it.hasNext()) {
      Vert n = it.next();
      //!>
      csv.writeLine(
              n.charge,
              n.distance,
              n.fluores[0].x,
              n.fluores[0].y,
              n.fluores[0].intensity,
              n.fluores[1].x,
              n.fluores[1].y,
              n.fluores[1].intensity,
              n.fluores[2].x,
              n.fluores[2].y,
              n.fluores[2].intensity,
              n.getCurvatureLocal(),
              n.curvatureSmoothed,
              n.curvatureSum,
              n.coord,
              n.gLandCoord,
              n.getPoint().x,
              n.getPoint().y,
              n.getNormal().x,
              n.getNormal().y,
              n.getTangent().x,
              n.getTangent().y,
              n.getPosition(),
              n.isFrozen() ? 1.0 : 0.0);
      //!<
    }
  }

  /**
   * Save statistic data associated with ANA analysis for all three channels.
   * 
   * <p>Produce files /path/core_cellNo_fluostats.csv with ANA data data for each cell along frames.
   * 
   * <p>Output file contains raw parameters that are also available in stQP file but not exactly the
   * same as some of stQP parameters are computed from those raw.
   * 
   * @throws QuimpException if stats are not available. Note that if ANA has not been run this
   *         method can still produce valid but empty output.
   * @see #saveStats()
   * @see #saveStatGeom()
   */
  public void saveStatFluores() throws QuimpException {
    if (qcL.isFileLoaded() == QParams.QUIMP_11) {
      throw new IllegalArgumentException("New format required.");
    }
    //!> order of data, must follow writeLine below
    final String[] params = {
        "#frame",
        "innerAreaCh1",
        "totalFluorCh1",
        "cortexWidthCh1",
        "meanFluorCh1",
        "meanInnerFluorCh1",
        "totalInnerFluorCh1",
        "cortexAreaCh1",
        "totalCorFluoCh1",
        "meanCorFluoCh1", 
        "percCortexFluoCh1", 
        "innerAreaCh2",
        "totalFluorCh2",
        "cortexWidthCh2",
        "meanFluorCh2",
        "meanInnerFluorCh2",
        "totalInnerFluorCh2",
        "cortexAreaCh2",
        "totalCorFluoCh2",
        "meanCorFluoCh2", 
        "percCortexFluoCh2",
        "innerAreaCh3",
        "totalFluorCh3",
        "cortexWidthCh3",
        "meanFluorCh3",
        "meanInnerFluorCh3",
        "totalInnerFluorCh3",
        "cortexAreaCh3",
        "totalCorFluoCh3",
        "meanCorFluoCh3", 
        "percCortexFluoCh3" 
        };
    //!<
    CsvWritter csv = null;
    int activeHandler = 0;
    StatsCollection st = qcL.getStats();
    for (CellStats cs : st.getStatCollection()) { // along cells
      try {
        csv = new CsvWritter(getFeatureFileName("fluostats", activeHandler, ".csv"), params);
        logger.info("\tSaved fluorosence stats at: " + csv.getPath().getFileName());
        for (FrameStatistics fs : cs.getFramestat()) { // along frames
          ChannelStat[] ch = fs.channels;
          //!>
          csv.writeLine(
                  (double)fs.frame,
                  ch[0].innerArea,
                  ch[0].totalFluor,
                  ch[0].cortexWidth,
                  ch[0].meanFluor,
                  ch[0].meanInnerFluor,
                  ch[0].totalInnerFluor,
                  ch[0].cortexArea,
                  ch[0].totalCorFluo,
                  ch[0].meanCorFluo,
                  ch[0].percCortexFluo,
                  ch[1].innerArea,
                  ch[1].totalFluor,
                  ch[1].cortexWidth,
                  ch[1].meanFluor,
                  ch[1].meanInnerFluor,
                  ch[1].totalInnerFluor,
                  ch[1].cortexArea,
                  ch[1].totalCorFluo,
                  ch[1].meanCorFluo,
                  ch[1].percCortexFluo,
                  ch[2].innerArea,
                  ch[2].totalFluor,
                  ch[2].cortexWidth,
                  ch[2].meanFluor,
                  ch[2].meanInnerFluor,
                  ch[2].totalInnerFluor,
                  ch[2].cortexArea,
                  ch[2].totalCorFluo,
                  ch[2].meanCorFluo,
                  ch[2].percCortexFluo
          );
          //!<
        }
      } catch (IOException e) {
        logger.error("Can not write file");
      } finally {
        activeHandler++;
        if (csv != null) {
          csv.close();
        }
      }
    }
  }

  /**
   * Save statistic data associated with ECMM analysis for all three channels.
   * 
   * <p>Produce files /path/core_cellNo_geomstats.csv with ANA data data for each cell along frames.
   * 
   * <p>Output file contains raw parameters that are also available in stQP file but not exactly the
   * same as some of stQP parameters are computed from those raw.
   * 
   * @throws QuimpException if stats are not available. Note that if ANA has not been run this
   *         method can still produce valid but empty output.
   * @see #saveStats()
   * @see #saveStatFluores()
   */
  public void saveStatGeom() throws QuimpException {
    if (qcL.isFileLoaded() == QParams.QUIMP_11) {
      throw new IllegalArgumentException("New format required.");
    }
    //!> order of data, must follow writeLine below
    final String[] params = {
        "#frame",
        "area",
        "elongation",
        "circularity",
        "perimiter",
        "displacement",
        "dist",
        "persistance",
        "speed",
        "persistanceToSource", 
        "dispersion", 
        "extension",
        "centroid_x",
        "centroid_y"
        };
    //!<
    CsvWritter csv = null;
    int activeHandler = 0;
    StatsCollection st = qcL.getStats();
    for (CellStats cs : st.getStatCollection()) { // along cells
      try {
        csv = new CsvWritter(getFeatureFileName("geomstats", activeHandler, ".csv"), params);
        logger.info("\tSaved geometrical stats at: " + csv.getPath().getFileName());
        for (FrameStatistics fs : cs.getFramestat()) { // along frames
          //!>
          csv.writeLine(
                  (double)fs.frame,
                  fs.area,
                  fs.elongation,
                  fs.circularity,
                  fs.perimiter,
                  fs.displacement,
                  fs.dist,
                  fs.persistance,
                  fs.speed,
                  fs.persistanceToSource,
                  fs.dispersion,
                  fs.extension,
                  fs.centroid.x,
                  fs.centroid.y
          );
        //!<
        }
      } catch (IOException e) {
        logger.error("Can not write file");
      } finally {
        activeHandler++;
        if (csv != null) {
          csv.close();
        }
      }
    }
  }

  /**
   * Save each outline centroid for each frame.
   * 
   * <p>Produce files /path/core_cellNo_ecmmcentroid.csv
   * 
   * @throws QuimpException if ECMM has not been run
   */
  public void saveEcmmCentroids() throws QuimpException {
    if (qcL.isFileLoaded() == QParams.QUIMP_11) {
      throw new IllegalArgumentException("New format required.");
    }
    int activeHandler = 0;
    CsvWritter csv = null;
    for (OutlineHandler oh : qcL.getEcmm().oHs) {
      try {
        csv = new CsvWritter(getFeatureFileName("ecmmcentroid", activeHandler, ".csv"), "#frame",
                "centroid_x", "centroid_y");
        logger.info("\tSaved ecmm centroids at: " + csv.getPath().getFileName());
        int sf = oh.getStartFrame();
        int ef = oh.getEndFrame();
        for (int f = sf; f <= ef; f++) {
          Outline outline = oh.getStoredOutline(f);
          ExtendedVector2d centroid = outline.getCentroid();
          csv.writeLine((double) f, centroid.x, centroid.y);
        }
      } catch (IOException e) {
        logger.error("Can not write file");
      } finally {
        activeHandler++;
        if (csv != null) {
          csv.close();
        }
      }
    }

  }

  /**
   * Create stQP file using internally stored Stats from QCONF.
   * 
   * @throws QuimpException when write of stQP file failed
   * @see #saveStatFluores()
   * @see #saveStatGeom()
   */
  public void saveStats() throws QuimpException {
    if (qcL.isFileLoaded() == QParams.QUIMP_11) {
      throw new IllegalArgumentException("Can not convert from old format to old");
    }
    int activeHandler = 0;
    DataContainer dt = ((QParamsQconf) qcL.getQp()).getLoadedDataContainer();
    Iterator<CellStats> csI = qcL.getStats().getStatCollection().iterator();
    do {
      Path p = getFeatureFileName("", activeHandler, FileExtensions.statsFileExt);
      ((QParamsQconf) qcL.getQp()).setActiveHandler(activeHandler);
      CellStats cs = csI.next();
      try {
        FrameStatistics.write(cs.getFramestat().toArray(new FrameStatistics[0]),
                ((QParamsQconf) qcL.getQp()).getStatsQP(), dt.BOAState.boap.getImageScale(),
                dt.BOAState.boap.getImageFrameInterval());
        logger.info("\tSaved stats at: " + p.getFileName());
      } catch (IOException e) {
        logger.error("Can not write file");
      } finally {
        activeHandler++;
      }
    } while (csI.hasNext());

  }

  /**
   * Produce file name basing on loaded QCONF (in the same folder and with the same core) extending
   * it by _cellNo and featName. Initializes also {@link BOAState} structures.
   * 
   * @param featName /path/core_cellNo_featName.ext
   * @param cellNo /path/core_cellNo_featName.ext
   * @param ext /path/core_cellNo_featName.ext (with dot)
   * @return /path/core_cellNo_featName.ext
   */
  Path getFeatureFileName(String featName, int cellNo, String ext) {
    DataContainer dt = ((QParamsQconf) qcL.getQp()).getLoadedDataContainer();
    dt.BOAState.boap.setOutputFileCore(path + File.separator + filename.toString());
    String fi = dt.BOAState.boap.getOutputFileCore().toPath().toString();
    fi = fi + "_" + cellNo + "_" + featName + ext;
    return Paths.get(fi);
  }

  /**
   * Create paQP and snQP file. Latter one contains only pure snake data.
   * 
   * <p>Those files are always saved together. snQP file will contain only pure snake data. Files
   * are created in directory where QCONF is located.
   * 
   * @throws IOException on writing snakes
   */
  private void generatepaQP() throws IOException {
    if (qcL.isFileLoaded() == QParams.QUIMP_11) {
      throw new IllegalArgumentException("Can not convert from old format to old");
    }
    logger.info("\tCreating configuration files");
    // replace location to location of QCONF
    DataContainer dt = ((QParamsQconf) qcL.getQp()).getLoadedDataContainer();
    dt.getBOAState().boap.setOutputFileCore(path + File.separator + filename.toString());
    logger.info("\tCreating snake files");
    dt.BOAState.nest.writeSnakes(); // write paQP and snQP together
  }

  /**
   * Rewrite snQP file using recent ECMM processed results.
   * 
   * <p>Files are created in directory where QCONF is located.
   * 
   * @throws IOException on writing old params
   * 
   */
  private void generatesnQP() throws IOException {
    if (qcL.isFileLoaded() == QParams.QUIMP_11) {
      throw new IllegalArgumentException("Can not convert from old format to old");
    }
    int activeHandler = 0;
    // replace location to location of QCONF
    DataContainer dt = ((QParamsQconf) qcL.getQp()).getLoadedDataContainer();
    dt.BOAState.boap.setOutputFileCore(path + File.separator + filename.toString());
    Iterator<OutlineHandler> ohi = dt.getEcmmState().oHs.iterator();
    do {
      logger.info("\tCreating snake file " + activeHandler);
      ((QParamsQconf) qcL.getQp()).setActiveHandler(activeHandler);
      OutlineHandler oh = ohi.next();
      oh.writeOutlines(((QParamsQconf) qcL.getQp()).getSnakeQP(), true);
      logger.info("\tCreating parameter file " + activeHandler);
      ((QParamsQconf) qcL.getQp()).writeOldParams();
      activeHandler++;
    } while (ohi.hasNext());

  }

  /**
   * Perform conversion depending on which file has been loaded.
   * 
   * <p>Supported conversions:
   * QCONF->paQP -- paQP, snQP, stQP, maQP files are generated (if data present in QCONF)
   * paQP->QCONF -- paQP, snQP, stQP, maQP are supported
   * 
   * @throws QuimpException on every error redirected to GUI. This is final method called from
   *         caller. All exceptions during conversion are collected and converted here to GUI.
   */
  public void doConversion() throws QuimpException {
    try {
      switch (qcL.isFileLoaded()) {
        case QParams.NEW_QUIMP:
          generateOldDataFiles();
          break;
        case QParams.QUIMP_11:
          Map<Integer, String> ret = QconfLoader.validatePaqp(
                  qcL.getQp().getPathasPath().resolve(qcL.getQp().getParamFile().toPath()));
          if (!ret.isEmpty()) {
            logger.warn("Sanity check returned the following warnings for paQP structure:");
            ret.values().stream().forEach(i -> Arrays.asList(i.split(QconfLoader.SEPARATOR))
                    .stream().forEach(j -> logger.warn(j)));
            logger.warn("FormatConverter will try to convert such file but it may"
                    + " lead to invalid QCONF file");
          }
          generateNewDataFiles();
          break;
        default:
          throw new IllegalArgumentException(
                  "QconfLoader returned unknown version of QuimP or error: " + qcL.isFileLoaded());
      }
    } catch (IOException qe) {
      throw new QuimpException(qe);
    }
  }

  /**
   * Return type of loaded file or 0 if not loaded yet.
   * 
   * @return {@link QconfLoader#QCONF_INVALID} or {@link QParams#NEW_QUIMP},
   *         {@link QParams#QUIMP_11}
   */
  public int isFileLoaded() {
    if (qcL == null) {
      return QconfLoader.QCONF_INVALID;
    } else {
      return qcL.isFileLoaded();
    }
  }

  /*
   * (non-Javadoc)
   * 
   * @see java.lang.Object#toString()
   */
  @Override
  public String toString() {
    String ret = "";
    switch (qcL.isFileLoaded()) {
      case QParams.NEW_QUIMP:
        ret = ret.concat("Experiment date: ")
                .concat(((QParamsQconf) qcL.getQp()).getFileVersion().getBuildstamp()).concat("\n");
        ret = ret.concat("File version: ")
                .concat(((QParamsQconf) qcL.getQp()).getFileVersion().getVersion()).concat("\n");
        ret = ret.concat("Is BOA analysis present? ").concat(" -- ")
                .concat(Boolean.toString(qcL.isBOAPresent())).concat("\n");
        ret = ret.concat("Is ECMM analysis present? ").concat(" -- ")
                .concat(Boolean.toString(qcL.isECMMPresent())).concat("\n");
        ret = ret.concat("Is ANA analysis present? ").concat(" -- ")
                .concat(Boolean.toString(qcL.isANAPresent())).concat("\n");
        ret = ret.concat("Is Q analysis present? ").concat(" -- ")
                .concat(Boolean.toString(qcL.isQPresent())).concat("\n");
        ret = ret.concat("Are stats present? ").concat(" -- ")
                .concat(Boolean.toString(qcL.isStatsPresent())).concat("\n");
        return ret;
      case QParams.QUIMP_11:
        ret = ret.concat(qcL.getQp().getFileName()).concat("\n");
        ret = ret.concat("Is file ").concat(qcL.getQp().getParamFile().getName())
                .concat(" present? ").concat(" -- ")
                .concat(Boolean.toString(qcL.getQp().getParamFile().exists())).concat("\n");
        ret = ret.concat("Is file ").concat(qcL.getQp().getSnakeQP().getName()).concat(" present? ")
                .concat(" -- ").concat(Boolean.toString(qcL.getQp().getSnakeQP().exists()))
                .concat("\n");
        ret = ret.concat("Is file ").concat(qcL.getQp().getStatsQP().getName()).concat(" present? ")
                .concat(" -- ").concat(Boolean.toString(qcL.getQp().getStatsQP().exists()))
                .concat("\n");
        ret = ret.concat("Is file ").concat(qcL.getQp().getMotilityFile().getName())
                .concat(" present? ").concat(" -- ")
                .concat(Boolean.toString(qcL.getQp().getMotilityFile().exists())).concat("\n");
        ret = ret.concat("Is file ").concat(qcL.getQp().getConvexFile().getName())
                .concat(" present? ").concat(" -- ")
                .concat(Boolean.toString(qcL.getQp().getConvexFile().exists())).concat("\n");
        ret = ret.concat("Is file ").concat(qcL.getQp().getCoordFile().getName())
                .concat(" present? ").concat(" -- ")
                .concat(Boolean.toString(qcL.getQp().getCoordFile().exists())).concat("\n");
        ret = ret.concat("Is file ").concat(qcL.getQp().getOriginFile().getName())
                .concat(" present? ").concat(" -- ")
                .concat(Boolean.toString(qcL.getQp().getOriginFile().exists())).concat("\n");
        ret = ret.concat("Is file ").concat(qcL.getQp().getxmapFile().getName())
                .concat(" present? ").concat(" -- ")
                .concat(Boolean.toString(qcL.getQp().getxmapFile().exists())).concat("\n");
        ret = ret.concat("Is file ").concat(qcL.getQp().getymapFile().getName())
                .concat(" present? ").concat(" -- ")
                .concat(Boolean.toString(qcL.getQp().getymapFile().exists())).concat("\n");
        File[] tmpf = qcL.getQp().getFluFiles();
        for (File f : tmpf) {
          ret = ret.concat("Is file ").concat(f.getName()).concat(" present? ").concat(" -- ")
                  .concat(Boolean.toString(f.exists())).concat("\n");
        }
        return ret;
      case QParams.OLD_QUIMP:
        ret = "toString is not supported for this format";
        return ret;
      default:
        return "No file loaded or file damaged";
    }
  }

}