View Javadoc
1   package com.github.celldynamics.quimp.filesystem.converter;
2   
3   import java.awt.Frame;
4   import java.io.File;
5   import java.io.IOException;
6   import java.nio.file.Path;
7   import java.nio.file.Paths;
8   import java.util.ArrayList;
9   import java.util.Arrays;
10  import java.util.Iterator;
11  import java.util.List;
12  import java.util.Map;
13  
14  import javax.swing.JOptionPane;
15  
16  import org.slf4j.LoggerFactory;
17  
18  import com.github.celldynamics.quimp.BOAState;
19  import com.github.celldynamics.quimp.BOA_;
20  import com.github.celldynamics.quimp.CellStats;
21  import com.github.celldynamics.quimp.FrameStatistics;
22  import com.github.celldynamics.quimp.Nest;
23  import com.github.celldynamics.quimp.Node;
24  import com.github.celldynamics.quimp.Outline;
25  import com.github.celldynamics.quimp.OutlineHandler;
26  import com.github.celldynamics.quimp.PointsList;
27  import com.github.celldynamics.quimp.QParams;
28  import com.github.celldynamics.quimp.QParamsQconf;
29  import com.github.celldynamics.quimp.QuimP;
30  import com.github.celldynamics.quimp.QuimpException;
31  import com.github.celldynamics.quimp.Serializer;
32  import com.github.celldynamics.quimp.Shape;
33  import com.github.celldynamics.quimp.Snake;
34  import com.github.celldynamics.quimp.SnakeHandler;
35  import com.github.celldynamics.quimp.Vert;
36  import com.github.celldynamics.quimp.filesystem.ANAParamCollection;
37  import com.github.celldynamics.quimp.filesystem.DataContainer;
38  import com.github.celldynamics.quimp.filesystem.FileExtensions;
39  import com.github.celldynamics.quimp.filesystem.OutlinesCollection;
40  import com.github.celldynamics.quimp.filesystem.QconfLoader;
41  import com.github.celldynamics.quimp.filesystem.StatsCollection;
42  import com.github.celldynamics.quimp.geom.ExtendedVector2d;
43  import com.github.celldynamics.quimp.plugin.ana.ANAp;
44  import com.github.celldynamics.quimp.plugin.ana.ChannelStat;
45  import com.github.celldynamics.quimp.plugin.qanalysis.FluoMap;
46  import com.github.celldynamics.quimp.plugin.qanalysis.Qp;
47  import com.github.celldynamics.quimp.plugin.qanalysis.STmap;
48  import com.github.celldynamics.quimp.utils.CsvWritter;
49  import com.github.celldynamics.quimp.utils.QuimPArrayUtils;
50  
51  import ch.qos.logback.classic.Logger;
52  
53  /**
54   * This class allows for converting between paQP and QCONF and vice versa.
55   * 
56   * <p>The following conversion are supported:<br>
57   * <b>paQP->QCONF</b><br>
58   * [+] paQP->QCONF<br>
59   * [+] snQP->QCONF<br>
60   * [+] maQP->QCONF<br>
61   * [+] stQP->QCONF<br>
62   * <b>QCONF->paQP</b><br>
63   * [+] QCONF->paQP<br>
64   * [+] QCONF->snQP<br>
65   * [+] QCONF->maQP<br>
66   * [+] QCONF->stQP<br>
67   * [-] QCONF->tiffs<br>
68   * 
69   * <p>This class can be also used to extract data from QCONF {@link DataContainer} and save them as
70   * plain csv files.
71   * 
72   * <p><b>Note</b>
73   * 
74   * <p>Images are generated regardless used file format in QuimP Q module.
75   * 
76   * <p>This method is related to fields that are non-transient and any change in serialised classes
77   * should be reflected here.
78   * 
79   * <p>Due to randomness during creating Snakes or Outlines (head node is picked randomly on
80   * {@link Shape#removePoint(PointsList, boolean)} these objects stored in QCONF may differ from snQP
81   * representation. There are the following rules ({@link DataContainer}):
82   * <ol>
83   * <li>paQP--QCONF conversion</li>
84   * <ol>
85   * <li>nest:liveSnake ({@link SnakeHandler#getLiveSnake()}) is filled but <b>should not be
86   * used</b></li>
87   * <li>nest:finalSnake ({@link SnakeHandler#getStoredSnake(int)}) is filled and it is converted from
88   * outlines (which are read from snQP file).
89   * Starting node may be different than in snQP due to conversion between Snakes and Outlines.
90   * <li>ECMMState:outlines ({@link DataContainer#getEcmmState()}) contains data read from
91   * snQP file if ECMM has been run (-ECMM string in snQP file header).
92   * <li>ANA (@link {@link DataContainer#getANAState()}) is created only if ECMM data is available.
93   * <li>Stats {@link DataContainer#getStats()} - read from stQP.csv file if present. If not present
94   * empty structure is created in QCONF but that file should be considered as defective.
95   * </ol>
96   * <li>QCONF--paQP</li>
97   * <ol>
98   * <li>Snakes in snQP are in the same order like:
99   * <ol>
100  * <li>nest:finalSnakes ({@link SnakeHandler#getStoredSnake(int)}) if there is no ECCM data
101  * (position is used)
102  * <li>ECMMState:outlines ({@link DataContainer#getEcmmState()}) if there is ECMM data (coord is
103  * used)
104  * </ol>
105  * </ol>
106  * </ol>
107  * 
108  * <p>Generally converter expects correct structure of paQP experiment, e.g. if there are many
109  * cells (_0.paQP, _1.paQP, etc), all cases should have other modules run on them with the same
110  * parameters (e.g. map resolution). There should be the same set of files for each case.
111  * 
112  * <p>See {@link #doConversion()} for supported files.
113  * 
114  * @author p.baniukiewicz
115  *
116  */
117 public class FormatConverter {
118   protected static Logger logger =
119           (Logger) LoggerFactory.getLogger(FormatConverter.class.getName());
120   private QconfLoader qcL;
121   private Path path; // path of file extracted from qcL
122   private Path filename; // file name extracted from qcL, no extension
123 
124   /**
125    * Order of parameters saved by {@link #saveOutline(Outline, CsvWritter)}.
126    */
127   public static final String[] headerEcmmOutline = {
128       //!> order of data, must follow writeLine below
129       "charge",
130       "distance",
131       "fluo-ch1_x",
132       "fluo-ch1_y",
133       "fluo-ch1_i",
134       "fluo-ch2_x",
135       "fluo-ch2_y",
136       "fluo-ch2_i",
137       "fluo-ch3_x", 
138       "fluo-ch3_y", 
139       "fluo-ch3_i",
140       "curvLoc",
141       "curvSmooth",
142       "curvSum",
143       "coord",
144       "gLandCoord",
145       "node_x",
146       "node_y",
147       "normal_x",
148       "normal_y",
149       "tan_x",
150       "tan_y",
151       "position",
152       "frozen"        
153       };
154   //!<
155 
156   /**
157    * Do nothing.
158    * 
159    * @see #attachFile(File)
160    */
161   public FormatConverter() {
162   }
163 
164   /**
165    * Construct FormatConverter from provided file.
166    * 
167    * @param fileToConvert file to convert
168    * @throws QuimpException if input file can not be loaded
169    */
170   public FormatConverter(File fileToConvert) throws QuimpException {
171     this();
172     attachFile(fileToConvert);
173   }
174 
175   /**
176    * Attach file for conversion.
177    * 
178    * <p>Do the same job as {@link #FormatConverter(File)} but can be used if
179    * {@link #FormatConverter()}
180    * was used.
181    * 
182    * @param fileToConvert file to convert
183    * @throws QuimpException if input file can not be loaded
184    */
185   public void attachFile(File fileToConvert) throws QuimpException {
186     logger.info("Converting file: " + fileToConvert.getName());
187     qcL = new QconfLoader(fileToConvert);
188     path = Paths.get(fileToConvert.getParent());
189     filename = Paths.get(qcL.getQp().getFileName()); // can contain xx_0 if old file loaded
190   }
191 
192   /**
193    * Construct conversion object from QconfLoader.
194    * 
195    * @param qcL reference to QconfLoader
196    */
197   public FormatConverter(QconfLoader qcL) {
198     logger.debug("Use provided QconfLoader");
199     this.qcL = qcL;
200     this.path = qcL.getQp().getPathasPath();
201     this.filename = Paths.get(qcL.getQp().getFileName()); // can contain xx_0 if old file loaded
202   }
203 
204   /**
205    * Show message with conversion capabilities.
206    * 
207    * @param frame parent frame
208    */
209   public void showConversionCapabilities(Frame frame) {
210     //!>
211     JOptionPane.showMessageDialog(frame,
212                 "Supported conversions\n"
213                 + "paQP->QCONF features:\n"
214                 + " [+] paQP->QCONF\n"
215                 + " [+] snQP->QCONF\n"
216                 + " [+] maQP->QCONF\n"
217                 + " [+] stQP->QCONF\n"
218                 + "QCONF->paQP features:\n"
219                 + " [+] QCONF->paQP\n"
220                 + " [+] QCONF->snQP\n"
221                 + " [+] QCONF->maQP\n"
222                 + " [+] QCONF->stQP\n"
223                 + " [-] QCONF->tiffs",
224                 "Warning",
225                 JOptionPane.WARNING_MESSAGE);
226     //!<
227   }
228 
229   /**
230    * Build QCONF from old datafile provided in constructor.
231    * 
232    * <p>Input file given in constructor is considered as starting one. paQP files in successive
233    * numbers are searched in the same directory. The internal <tt>qcL</tt> variable will be
234    * overrode on this method call.
235    * 
236    * @throws QuimpException on wrong inputs
237    * @throws IOException on saving QCONF
238    */
239   private void generateNewDataFiles() throws QuimpException, IOException {
240     if (qcL.isFileLoaded() == QParams.NEW_QUIMP) {
241       throw new IllegalArgumentException("Can not convert from new format to new");
242     }
243     boolean ecmmRun = false;
244     logger.info("File is in old format, new format will be created");
245     // create storage
246     DataContaineruimp/filesystem/DataContainer.html#DataContainer">DataContainer dt = new DataContainer();
247     dt.BOAState = new BOAState(qcL.getImage());
248     dt.ECMMState = null;
249     dt.BOAState.nest = new Nest();
250     // dT.ANAState = new ANAStates();
251     ArrayList<STmap> maps = new ArrayList<>(); // temporary - we do not know number of cells
252 
253     // extract paQP number from file name (loaded in constructor)
254     int last = filename.toString().lastIndexOf('_'); // position of _ before number
255     if (last < 0) {
256       throw new QuimpException(
257               "Input file name must be in format name_XX.paQP, where XX is cell number.");
258     }
259 
260     // check which file number user selected. End program if user made mistake
261     try {
262       int numofpaqp; // number extracted from paQP name
263       numofpaqp = Integer
264               .parseInt(filename.toString().substring(last + 1, filename.toString().length()));
265       if (numofpaqp != 0) { // warn user if not first file selected
266         throw new QuimpException("Selected paQP file is not first (not a _0.paQP file).");
267       }
268     } catch (NumberFormatException e) {
269       throw new QuimpException("Number can not be found in file paQP name. "
270               + "Check if file name is in format name_XX.paQP, where XX is cell number.");
271     }
272     // cut last number from file name name
273     String orginal = filename.toString().substring(0, last);
274 
275     int i; // PaQP files counter
276     // run conversion for all paQP files. conversion always starts from 0
277     i = 0;
278     File filetoload = new File(""); // store name_XX.paQP file in loop below
279     OutlineHandler oh;
280     STmap stMap;
281     ANAParamCollectionlesystem/ANAParamCollection.html#ANAParamCollection">ANAParamCollection anaP = new ANAParamCollection(); // holder for ANA config, for every cell
282     try {
283       do {
284         // paQP files with _xx number in name
285         filetoload =
286                 Paths.get(qcL.getQp().getPath(), orginal + "_" + i + FileExtensions.configFileExt)
287                         .toFile();
288         logger.info("Attempting to process " + filetoload.getName());
289         if (!filetoload.exists()) { // if does not exist - end loop
290           logger.info("File " + filetoload.toString() + " does not exist. Finishing conversion.");
291           break;
292         }
293         // optimisation - first file is already loaded, skip it
294         if (i != 0) {
295           qcL = new QconfLoader(filetoload); // re-load it
296         } else {
297           // assume that BOA params are taken from file 0_.paQP
298           dt.BOAState.loadParams(qcL.getQp()); // load parameters only once (but not frameinterval,)
299           dt.BOAState.boap.setImageFrameInterval(qcL.getQp().getFrameInterval());
300           dt.BOAState.boap.setImageScale(qcL.getQp().getImageScale());
301         }
302         logger.info(toString());
303         // initialize snakes (from snQP files)
304         logger.info("... Reading snakes");
305         // check if ECMM was run on this snake file
306         ecmmRun = qcL.getQp().verifyEcmminpsnQP();
307         BOA_.qState = dt.BOAState; // for compatibility - create static
308         oh = new OutlineHandler(qcL.getQp()); // restore OutlineHandler
309         if (ecmmRun) { // create structure if ecmm was run (assume that all paQP was then processed)
310           if (dt.ECMMState == null) { // do not overwrite for many paQP
311             dt.ECMMState = new OutlinesCollection();
312           }
313           dt.ECMMState.oHs.add(oh); // store in ECMM object
314         } else {
315           logger.info("... Reading snakes - no ECMM data");
316         }
317         dt.BOAState.nest.addOutlinehandler(oh); // covert ECMM to Snake and store in BOA section
318 
319         // load maps and store in QCONF
320         stMap = new STmap();
321         int readMap = 0; // check if we loaded at least one we expected 5 remaining as well
322         try {
323           logger.info("\tReading " + qcL.getQp().getMotilityFile().getName());
324           stMap.setMotMap(QuimPArrayUtils.file2Array(",", qcL.getQp().getMotilityFile()));
325           readMap++;
326         } catch (IOException e) {
327           logger.info(e.getMessage());
328           logger.debug(e.getMessage(), e);
329         }
330         try {
331           logger.info("\tReading " + qcL.getQp().getConvexFile().getName());
332           stMap.setConvMap(QuimPArrayUtils.file2Array(",", qcL.getQp().getConvexFile()));
333           readMap++;
334         } catch (IOException e) {
335           logger.info(e.getMessage());
336           logger.debug(e.getMessage(), e);
337         }
338         try {
339           logger.info("\tReading " + qcL.getQp().getCoordFile().getName());
340           stMap.setCoordMap(QuimPArrayUtils.file2Array(",", qcL.getQp().getCoordFile()));
341           readMap++;
342         } catch (IOException e) {
343           logger.info(e.getMessage());
344           logger.debug(e.getMessage(), e);
345         }
346         try {
347           logger.info("\tReading " + qcL.getQp().getOriginFile().getName());
348           stMap.setOriginMap(QuimPArrayUtils.file2Array(",", qcL.getQp().getOriginFile()));
349           readMap++;
350         } catch (IOException e) {
351           logger.debug(e.getMessage(), e);
352           logger.info(e.getMessage());
353         }
354         try {
355           logger.info("\tReading " + qcL.getQp().getxmapFile().getName());
356           stMap.setxMap(QuimPArrayUtils.file2Array(",", qcL.getQp().getxmapFile()));
357           readMap++;
358         } catch (IOException e) {
359           logger.info(e.getMessage());
360           logger.debug(e.getMessage(), e);
361         }
362         try {
363           logger.info("\tReading " + qcL.getQp().getymapFile().getName());
364           stMap.setyMap(QuimPArrayUtils.file2Array(",", qcL.getQp().getymapFile()));
365           readMap++;
366         } catch (IOException e) {
367           logger.info(e.getMessage());
368           logger.debug(e.getMessage(), e);
369         }
370         // number of read maps check (expecting all read if there is at leas one)
371         if (readMap >= 1 && readMap < 6) {
372           logger.warn("It seems that you have missing maps. Perhaps your dataset is incomplete. "
373                   + "This may lead to invalid QCONF file.");
374         }
375         // Fluoromap
376         // first check if there is any FluMap
377         int channel = 1; // channel counter for fluoromaps
378         int p = 0; // sizes of flumap
379         int t = 0; // sizes of flumap
380         for (File ff : qcL.getQp().getFluFiles()) { // iterate over filenames
381           // if any exist, get its size. Because if there is no maps at all we set this object to
382           // null but if there is at least one we have to set all other maps to -1 array of size of
383           // that available one
384           if (ff.exists()) {
385             double[][] tmpFluMap = QuimPArrayUtils.file2Array(",", ff);
386             t = tmpFluMap.length;
387             p = tmpFluMap[0].length;
388             // Fluoromap exist, so other must do as they are generated together, check this
389             if (stMap.getT() == 0) { // no map loaded above (fluoro and other are generated togethe)
390               logger.warn("It seems that you have fluorosence map but other maps are missing."
391                       + " Perhaps your dataset is incomplete. "
392                       + "This may lead to invalid QCONF file.");
393             }
394             break; // assume without checking that all maps are the same
395           }
396         }
397         // if p,t are non zero we know that there is at least one map
398         // try to read 3 channels for current paQP
399         for (File ff : qcL.getQp().getFluFiles()) {
400           // create Fluoro data holder
401           FluoMapics/quimp/plugin/qanalysis/FluoMap.html#FluoMap">FluoMap chm = new FluoMap(t, p, channel);
402           if (ff.exists()) { // read file stored in paQP for channel
403             logger.info("\tReading " + ff.getName());
404             chm.setMap(QuimPArrayUtils.file2Array(",", ff)); // it sets it enabled
405           } else {
406             chm.setEnabled(false); // not existing, disable
407             logger.info("File " + ff.toString() + " not found.");
408           }
409           stMap.fluoMaps[channel - 1] = chm;
410           channel++;
411         }
412         // add container if there is at least one map
413         if (stMap.getT() != 0 || t != 0) {
414           maps.add(stMap);
415         }
416         // ANAState - add ANAp references for every processed paQP, set only non-transient fields
417         logger.info("\tFilling ANA");
418         ANApcs/quimp/plugin/ana/ANAp.html#ANAp">ANAp anapTmp = new ANAp();
419         anapTmp.scale = qcL.getQp().getImageScale(); // set scale used by setCortextWidthScale
420         anapTmp.setCortextWidthScale(qcL.getQp().cortexWidth); // sets also cortexWidthPixel
421         anapTmp.fluTiffs = qcL.getQp().fluTiffs; // set files
422         anaP.aS.add(anapTmp); // store in ANAParamCollection
423 
424         i++; // go to next paQP
425       } while (true); // exception thrown by QconfLoader will stop this loop, e.g. trying to load
426       // nonexiting file
427 
428       // process stQP - all files
429       logger.info("\tReading stats");
430 
431       StatFileParsermp/filesystem/converter/StatFileParser.html#StatFileParser">StatFileParser obj = new StatFileParser(Paths.get(qcL.getQp().getPath(), orginal).toString());
432       List<Path> statFiles = obj.getAllFiles(); // count stQP files
433       // and do simple checking
434       if (i != statFiles.size()) { // i counted from 0
435         logger.warn("It seems that number of stQP files is different than number of paQP files."
436                 + " Perhaps your dataset is incomplete. This may lead to invalid QCONF file.");
437       }
438       ArrayList<CellStats> stats = obj.importStQp();
439       dt.Stats = new StatsCollection();
440       dt.Stats.setStatCollection(stats);
441 
442     } catch (Exception e) { // repack exception with proper message about defective file
443       throw new QuimpException(
444               "File " + filetoload.toString() + " can not be processed: " + e.getMessage(), e);
445     }
446     // do simple map checking - find if all (none) paQP had (had not) maps.
447     int count = 0;
448     for (STmap tmp : maps) {
449       if (tmp.getT() == 0) {
450         count++;
451       } else {
452         count--;
453       }
454     }
455     if (Math.abs(count) != maps.size()) {
456       logger.warn("It seems that some paQP cases have missing maps."
457               + " Perhaps your dataset is incomplete. This may lead to invalid QCONF file.");
458     }
459     // save DataContainer using Serializer
460     if (!maps.isEmpty()) {
461       dt.QState = maps.toArray(new STmap[0]); // convert to array
462     } else {
463       dt.QState = null;
464     }
465     if (ecmmRun) {
466       dt.ANAState = anaP;
467     } else {
468       dt.ANAState = null;
469     }
470 
471     Serializer<DataContainer> n;
472     n = new Serializer<>(dt, QuimP.TOOL_VERSION);
473     n.setPretty();
474     n.save(path + File.separator + orginal + FileExtensions.newConfigFileExt);
475     n = null;
476   }
477 
478   /**
479    * Recreate paQP, snQP, stQP and maQP files from QCONF.
480    * 
481    * <p>Files are created in directory where QCONF is located.
482    * 
483    * @throws IOException on file saving error
484    * @throws QuimpException if requested data are not available in QCONF
485    * 
486    */
487   private void generateOldDataFiles() throws IOException, QuimpException {
488     if (qcL.isFileLoaded() == QParams.QUIMP_11) {
489       throw new IllegalArgumentException("Can not convert from old format to old");
490     }
491     logger.info("File is in new format, old format will be created");
492     logger.info(toString());
493     DataContainer dt = ((QParamsQconf) qcL.getQp()).getLoadedDataContainer();
494     if (dt.getEcmmState() == null) {
495       logger.warn("ECMM analysis is not present in QCONF");
496       generatepaQP(); // no ecmm data write snakes only
497     } else {
498       generatesnQP(); // write ecmm data
499     }
500     if (qcL.isStatsPresent()) {
501       saveStats();
502     } else {
503       logger.warn("Statistics are not present in QCONF");
504     }
505     if (qcL.isQPresent()) {
506       saveMaps(STmap.ALLMAPS);
507     } else {
508       logger.warn("Q analysis is not present in QCONF");
509     }
510   }
511 
512   /**
513    * Save selected maps to files. Support only new file format. Throws
514    * {@link IllegalArgumentException} when run from object constructed from old format.
515    * 
516    * <p>Follow naming convention: <i>ROOT_N_MAPNAME.maQP</i>, where <tt>ROOT</tt> is corename of
517    * QCONF file,(<i>ROOT.QCONF</i>), <tt>N</tt> is cell number and <tt>MAPNAME</tt> follows
518    * supported map extensions.
519    * 
520    * @param maps map defined in {@link STmap}
521    * @throws QuimpException if maps are not available
522    */
523   public void saveMaps(int maps) throws QuimpException {
524     if (qcL.isFileLoaded() == QParams.QUIMP_11) {
525       throw new IllegalArgumentException("New format required.");
526     }
527     int activeHandler = 0;
528     // replace location to location of QCONF
529     DataContainer dt = ((QParamsQconf) qcL.getQp()).getLoadedDataContainer();
530     dt.BOAState.boap.setOutputFileCore(path + File.separator + filename.toString());
531     String name = STmap.LOGGER.getName();
532     STmap.LOGGER = logger; // FIXME replace
533     Qpamics/quimp/plugin/qanalysis/Qp.html#Qp">Qp params = new Qp();
534     try {
535       for (STmap stmap : qcL.getQ()) {
536         ((QParamsQconf) qcL.getQp()).setActiveHandler(activeHandler++);
537         stmap.setParams(params);
538         params.setup(qcL.getQp());
539         stmap.saveMaps(maps);
540       }
541     } finally {
542       STmap.LOGGER = LoggerFactory.getLogger(name);
543     }
544   }
545 
546   /**
547    * Save each snake centroid for each frame.
548    * 
549    * <p>Produce files /path/core_cellNo_boacentroid.csv
550    * 
551    * @throws QuimpException if BOA structure is not available
552    */
553   public void saveBoaCentroids() throws QuimpException {
554     if (qcL.isFileLoaded() == QParams.QUIMP_11) {
555       throw new IllegalArgumentException("New format required.");
556     }
557     int activeHandler = 0;
558     CsvWritter csv = null;
559     for (SnakeHandler sh : qcL.getBOA().nest.getHandlers()) {
560       try {
561         csv = new CsvWritter(getFeatureFileName("boacentroid", activeHandler, ".csv"), "#frame",
562                 "centroid_x", "centroid_y");
563         logger.info("\tSaved Boa centroids at: " + csv.getPath().getFileName());
564         int sf = sh.getStartFrame();
565         int ef = sh.getEndFrame();
566         for (int f = sf; f <= ef; f++) {
567           Snake snake = sh.getStoredSnake(f);
568           ExtendedVector2d centroid = snake.getCentroid();
569           csv.writeLine((double) f, centroid.x, centroid.y);
570         }
571       } catch (IOException e) {
572         logger.error("Can not write file");
573       } finally {
574         activeHandler++;
575         if (csv != null) {
576           csv.close();
577         }
578       }
579     }
580 
581   }
582 
583   /**
584    * Save all data associated with BOA analysis.
585    * 
586    * <p>Produce files /path/core_cellNo_snake-frame_no.csv with snake data for each frame separately
587    * or files /path/core_cellNo_snake.csv with all data in one file (for same snake).
588    * 
589    * <p>Output file contain only parameters directly included in QCONF in contrary to e.g. snQP
590    * files (or results of QCONF->paQP conversion) that contain some extra data calculated.
591    * 
592    * @param separateFiles Control whether files should be broken into separate files for each frame
593    *        (true) and for each object or store all frames in one file (false).
594    * @throws QuimpException if BOA structure is not available
595    */
596   public void saveBoaSnakes(boolean separateFiles) throws QuimpException {
597     if (qcL.isFileLoaded() == QParams.QUIMP_11) {
598       throw new IllegalArgumentException("New format required.");
599     }
600     //!> order of data, must follow writeLine below
601     final String[] params = {
602         "vel_x",
603         "vel_y",
604         "F-total_x",
605         "F-total_y",
606         "node_x",
607         "node_y",
608         "normal_x",
609         "normal_y",
610         "tan_x",
611         "tan_y",
612         "position",
613         "frozen"};
614     //!<
615     CsvWritter csv = null;
616     int activeHandler = 0;
617     for (SnakeHandler sh : qcL.getBOA().nest.getHandlers()) {
618       int sf = sh.getStartFrame();
619       int ef = sh.getEndFrame();
620       try {
621         for (int f = sf; f <= ef; f++) {
622           Snake snake = sh.getStoredSnake(f);
623           if (separateFiles == true) { // create for each frame
624             csv = new CsvWritter(getFeatureFileName("snake-frame" + f, activeHandler, ".csv"),
625                     params);
626           } else if (f == sf) { // create only once on first frame
627             csv = new CsvWritter(getFeatureFileName("snake", activeHandler, ".csv"), params);
628 
629           }
630           if (separateFiles == false) {
631             csv.writeLine("#frame " + f); // just add break if one file outputed
632           }
633           logger.info("\tSaved snakes at: " + csv.getPath().getFileName());
634           Iterator<Node> it = snake.iterator();
635           while (it.hasNext()) {
636             Node n = it.next();
637             //!>
638             csv.writeLine(
639                     n.getVel().x,
640                     n.getVel().y,
641                     n.getF_total().x,
642                     n.getF_total().y,
643                     n.getPoint().x,
644                     n.getPoint().y,
645                     n.getNormal().x,
646                     n.getNormal().y,
647                     n.getTangent().x,
648                     n.getTangent().y,
649                     n.getPosition(),
650                     n.isFrozen() ? 1.0 : 0.0);
651             //!<
652           }
653           if (separateFiles == true) {
654             csv.close(); // after frame
655           }
656         }
657       } catch (IOException e) {
658         logger.error("Can not write file");
659       } finally {
660         activeHandler++;
661         if (csv != null) {
662           csv.close();
663         }
664       }
665     }
666   }
667 
668   /**
669    * Save all data associated with ECMM and ANA analysis.
670    * 
671    * <p>Produce files /path/core_cellNo_outline-frame_no.csv with snake data for each frame
672    * separately or files /path/core_cellNo_outline.csv with all data in one file (for same snake).
673    * 
674    * <p>Output file contain only parameters directly included in QCONF in contrary to e.g. snQP
675    * files (or results of QCONF->paQP conversion) that contain some extra data calculated.
676    * 
677    * @param separateFiles Control whether files should be broken into separate files for each frame
678    *        (true) and for each object or store all frames in one file (false).
679    * @throws QuimpException if ECMM has not been run
680    */
681   public void saveEcmmOutlines(boolean separateFiles) throws QuimpException {
682     if (qcL.isFileLoaded() == QParams.QUIMP_11) {
683       throw new IllegalArgumentException("New format required.");
684     }
685     CsvWritter csv = null;
686     int activeHandler = 0;
687     for (OutlineHandler oh : qcL.getEcmm().oHs) {
688       int sf = oh.getStartFrame();
689       int ef = oh.getEndFrame();
690       try {
691         for (int f = sf; f <= ef; f++) {
692           Outline outline = oh.getStoredOutline(f);
693           if (separateFiles == true) { // create for each frame
694             csv = new CsvWritter(getFeatureFileName("outline-frame" + f, activeHandler, ".csv"),
695                     headerEcmmOutline);
696           } else if (f == sf) { // create only once on first frame
697             csv = new CsvWritter(getFeatureFileName("outline", activeHandler, ".csv"),
698                     headerEcmmOutline);
699           }
700           if (separateFiles == false) {
701             csv.writeLine("#frame " + f); // just add break if one file outputed
702           }
703           saveOutline(outline, csv);
704           if (separateFiles == true) {
705             csv.close(); // after frame
706           }
707         }
708       } catch (IOException e) {
709         logger.error("Can not write file");
710       } finally {
711         activeHandler++;
712         if (csv != null) {
713           csv.close();
714         }
715       }
716     }
717   }
718 
719   /**
720    * Save specified outline to {@link CsvWritter}.
721    * 
722    * @param outline outline to save
723    * @param csv opened csv object
724    */
725   public static void saveOutline(Outline outline, CsvWritter csv) {
726     logger.info("\tSaved outlines at: " + csv.getPath().getFileName());
727     Iterator<Vert> it = outline.iterator();
728     while (it.hasNext()) {
729       Vert n = it.next();
730       //!>
731       csv.writeLine(
732               n.charge,
733               n.distance,
734               n.fluores[0].x,
735               n.fluores[0].y,
736               n.fluores[0].intensity,
737               n.fluores[1].x,
738               n.fluores[1].y,
739               n.fluores[1].intensity,
740               n.fluores[2].x,
741               n.fluores[2].y,
742               n.fluores[2].intensity,
743               n.getCurvatureLocal(),
744               n.curvatureSmoothed,
745               n.curvatureSum,
746               n.coord,
747               n.gLandCoord,
748               n.getPoint().x,
749               n.getPoint().y,
750               n.getNormal().x,
751               n.getNormal().y,
752               n.getTangent().x,
753               n.getTangent().y,
754               n.getPosition(),
755               n.isFrozen() ? 1.0 : 0.0);
756       //!<
757     }
758   }
759 
760   /**
761    * Save statistic data associated with ANA analysis for all three channels.
762    * 
763    * <p>Produce files /path/core_cellNo_fluostats.csv with ANA data data for each cell along frames.
764    * 
765    * <p>Output file contains raw parameters that are also available in stQP file but not exactly the
766    * same as some of stQP parameters are computed from those raw.
767    * 
768    * @throws QuimpException if stats are not available. Note that if ANA has not been run this
769    *         method can still produce valid but empty output.
770    * @see #saveStats()
771    * @see #saveStatGeom()
772    */
773   public void saveStatFluores() throws QuimpException {
774     if (qcL.isFileLoaded() == QParams.QUIMP_11) {
775       throw new IllegalArgumentException("New format required.");
776     }
777     //!> order of data, must follow writeLine below
778     final String[] params = {
779         "#frame",
780         "innerAreaCh1",
781         "totalFluorCh1",
782         "cortexWidthCh1",
783         "meanFluorCh1",
784         "meanInnerFluorCh1",
785         "totalInnerFluorCh1",
786         "cortexAreaCh1",
787         "totalCorFluoCh1",
788         "meanCorFluoCh1", 
789         "percCortexFluoCh1", 
790         "innerAreaCh2",
791         "totalFluorCh2",
792         "cortexWidthCh2",
793         "meanFluorCh2",
794         "meanInnerFluorCh2",
795         "totalInnerFluorCh2",
796         "cortexAreaCh2",
797         "totalCorFluoCh2",
798         "meanCorFluoCh2", 
799         "percCortexFluoCh2",
800         "innerAreaCh3",
801         "totalFluorCh3",
802         "cortexWidthCh3",
803         "meanFluorCh3",
804         "meanInnerFluorCh3",
805         "totalInnerFluorCh3",
806         "cortexAreaCh3",
807         "totalCorFluoCh3",
808         "meanCorFluoCh3", 
809         "percCortexFluoCh3" 
810         };
811     //!<
812     CsvWritter csv = null;
813     int activeHandler = 0;
814     StatsCollection st = qcL.getStats();
815     for (CellStats cs : st.getStatCollection()) { // along cells
816       try {
817         csv = new CsvWritter(getFeatureFileName("fluostats", activeHandler, ".csv"), params);
818         logger.info("\tSaved fluorosence stats at: " + csv.getPath().getFileName());
819         for (FrameStatistics fs : cs.getFramestat()) { // along frames
820           ChannelStat[] ch = fs.channels;
821           //!>
822           csv.writeLine(
823                   (double)fs.frame,
824                   ch[0].innerArea,
825                   ch[0].totalFluor,
826                   ch[0].cortexWidth,
827                   ch[0].meanFluor,
828                   ch[0].meanInnerFluor,
829                   ch[0].totalInnerFluor,
830                   ch[0].cortexArea,
831                   ch[0].totalCorFluo,
832                   ch[0].meanCorFluo,
833                   ch[0].percCortexFluo,
834                   ch[1].innerArea,
835                   ch[1].totalFluor,
836                   ch[1].cortexWidth,
837                   ch[1].meanFluor,
838                   ch[1].meanInnerFluor,
839                   ch[1].totalInnerFluor,
840                   ch[1].cortexArea,
841                   ch[1].totalCorFluo,
842                   ch[1].meanCorFluo,
843                   ch[1].percCortexFluo,
844                   ch[2].innerArea,
845                   ch[2].totalFluor,
846                   ch[2].cortexWidth,
847                   ch[2].meanFluor,
848                   ch[2].meanInnerFluor,
849                   ch[2].totalInnerFluor,
850                   ch[2].cortexArea,
851                   ch[2].totalCorFluo,
852                   ch[2].meanCorFluo,
853                   ch[2].percCortexFluo
854           );
855           //!<
856         }
857       } catch (IOException e) {
858         logger.error("Can not write file");
859       } finally {
860         activeHandler++;
861         if (csv != null) {
862           csv.close();
863         }
864       }
865     }
866   }
867 
868   /**
869    * Save statistic data associated with ECMM analysis for all three channels.
870    * 
871    * <p>Produce files /path/core_cellNo_geomstats.csv with ANA data data for each cell along frames.
872    * 
873    * <p>Output file contains raw parameters that are also available in stQP file but not exactly the
874    * same as some of stQP parameters are computed from those raw.
875    * 
876    * @throws QuimpException if stats are not available. Note that if ANA has not been run this
877    *         method can still produce valid but empty output.
878    * @see #saveStats()
879    * @see #saveStatFluores()
880    */
881   public void saveStatGeom() throws QuimpException {
882     if (qcL.isFileLoaded() == QParams.QUIMP_11) {
883       throw new IllegalArgumentException("New format required.");
884     }
885     //!> order of data, must follow writeLine below
886     final String[] params = {
887         "#frame",
888         "area",
889         "elongation",
890         "circularity",
891         "perimiter",
892         "displacement",
893         "dist",
894         "persistance",
895         "speed",
896         "persistanceToSource", 
897         "dispersion", 
898         "extension",
899         "centroid_x",
900         "centroid_y"
901         };
902     //!<
903     CsvWritter csv = null;
904     int activeHandler = 0;
905     StatsCollection st = qcL.getStats();
906     for (CellStats cs : st.getStatCollection()) { // along cells
907       try {
908         csv = new CsvWritter(getFeatureFileName("geomstats", activeHandler, ".csv"), params);
909         logger.info("\tSaved geometrical stats at: " + csv.getPath().getFileName());
910         for (FrameStatistics fs : cs.getFramestat()) { // along frames
911           //!>
912           csv.writeLine(
913                   (double)fs.frame,
914                   fs.area,
915                   fs.elongation,
916                   fs.circularity,
917                   fs.perimiter,
918                   fs.displacement,
919                   fs.dist,
920                   fs.persistance,
921                   fs.speed,
922                   fs.persistanceToSource,
923                   fs.dispersion,
924                   fs.extension,
925                   fs.centroid.x,
926                   fs.centroid.y
927           );
928         //!<
929         }
930       } catch (IOException e) {
931         logger.error("Can not write file");
932       } finally {
933         activeHandler++;
934         if (csv != null) {
935           csv.close();
936         }
937       }
938     }
939   }
940 
941   /**
942    * Save each outline centroid for each frame.
943    * 
944    * <p>Produce files /path/core_cellNo_ecmmcentroid.csv
945    * 
946    * @throws QuimpException if ECMM has not been run
947    */
948   public void saveEcmmCentroids() throws QuimpException {
949     if (qcL.isFileLoaded() == QParams.QUIMP_11) {
950       throw new IllegalArgumentException("New format required.");
951     }
952     int activeHandler = 0;
953     CsvWritter csv = null;
954     for (OutlineHandler oh : qcL.getEcmm().oHs) {
955       try {
956         csv = new CsvWritter(getFeatureFileName("ecmmcentroid", activeHandler, ".csv"), "#frame",
957                 "centroid_x", "centroid_y");
958         logger.info("\tSaved ecmm centroids at: " + csv.getPath().getFileName());
959         int sf = oh.getStartFrame();
960         int ef = oh.getEndFrame();
961         for (int f = sf; f <= ef; f++) {
962           Outline outline = oh.getStoredOutline(f);
963           ExtendedVector2d centroid = outline.getCentroid();
964           csv.writeLine((double) f, centroid.x, centroid.y);
965         }
966       } catch (IOException e) {
967         logger.error("Can not write file");
968       } finally {
969         activeHandler++;
970         if (csv != null) {
971           csv.close();
972         }
973       }
974     }
975 
976   }
977 
978   /**
979    * Create stQP file using internally stored Stats from QCONF.
980    * 
981    * @throws QuimpException when write of stQP file failed
982    * @see #saveStatFluores()
983    * @see #saveStatGeom()
984    */
985   public void saveStats() throws QuimpException {
986     if (qcL.isFileLoaded() == QParams.QUIMP_11) {
987       throw new IllegalArgumentException("Can not convert from old format to old");
988     }
989     int activeHandler = 0;
990     DataContainer dt = ((QParamsQconf) qcL.getQp()).getLoadedDataContainer();
991     Iterator<CellStats> csI = qcL.getStats().getStatCollection().iterator();
992     do {
993       Path p = getFeatureFileName("", activeHandler, FileExtensions.statsFileExt);
994       ((QParamsQconf) qcL.getQp()).setActiveHandler(activeHandler);
995       CellStats cs = csI.next();
996       try {
997         FrameStatistics.write(cs.getFramestat().toArray(new FrameStatistics[0]),
998                 ((QParamsQconf) qcL.getQp()).getStatsQP(), dt.BOAState.boap.getImageScale(),
999                 dt.BOAState.boap.getImageFrameInterval());
1000         logger.info("\tSaved stats at: " + p.getFileName());
1001       } catch (IOException e) {
1002         logger.error("Can not write file");
1003       } finally {
1004         activeHandler++;
1005       }
1006     } while (csI.hasNext());
1007 
1008   }
1009 
1010   /**
1011    * Produce file name basing on loaded QCONF (in the same folder and with the same core) extending
1012    * it by _cellNo and featName. Initializes also {@link BOAState} structures.
1013    * 
1014    * @param featName /path/core_cellNo_featName.ext
1015    * @param cellNo /path/core_cellNo_featName.ext
1016    * @param ext /path/core_cellNo_featName.ext (with dot)
1017    * @return /path/core_cellNo_featName.ext
1018    */
1019   Path getFeatureFileName(String featName, int cellNo, String ext) {
1020     DataContainer dt = ((QParamsQconf) qcL.getQp()).getLoadedDataContainer();
1021     dt.BOAState.boap.setOutputFileCore(path + File.separator + filename.toString());
1022     String fi = dt.BOAState.boap.getOutputFileCore().toPath().toString();
1023     fi = fi + "_" + cellNo + "_" + featName + ext;
1024     return Paths.get(fi);
1025   }
1026 
1027   /**
1028    * Create paQP and snQP file. Latter one contains only pure snake data.
1029    * 
1030    * <p>Those files are always saved together. snQP file will contain only pure snake data. Files
1031    * are created in directory where QCONF is located.
1032    * 
1033    * @throws IOException on writing snakes
1034    */
1035   private void generatepaQP() throws IOException {
1036     if (qcL.isFileLoaded() == QParams.QUIMP_11) {
1037       throw new IllegalArgumentException("Can not convert from old format to old");
1038     }
1039     logger.info("\tCreating configuration files");
1040     // replace location to location of QCONF
1041     DataContainer dt = ((QParamsQconf) qcL.getQp()).getLoadedDataContainer();
1042     dt.getBOAState().boap.setOutputFileCore(path + File.separator + filename.toString());
1043     logger.info("\tCreating snake files");
1044     dt.BOAState.nest.writeSnakes(); // write paQP and snQP together
1045   }
1046 
1047   /**
1048    * Rewrite snQP file using recent ECMM processed results.
1049    * 
1050    * <p>Files are created in directory where QCONF is located.
1051    * 
1052    * @throws IOException on writing old params
1053    * 
1054    */
1055   private void generatesnQP() throws IOException {
1056     if (qcL.isFileLoaded() == QParams.QUIMP_11) {
1057       throw new IllegalArgumentException("Can not convert from old format to old");
1058     }
1059     int activeHandler = 0;
1060     // replace location to location of QCONF
1061     DataContainer dt = ((QParamsQconf) qcL.getQp()).getLoadedDataContainer();
1062     dt.BOAState.boap.setOutputFileCore(path + File.separator + filename.toString());
1063     Iterator<OutlineHandler> ohi = dt.getEcmmState().oHs.iterator();
1064     do {
1065       logger.info("\tCreating snake file " + activeHandler);
1066       ((QParamsQconf) qcL.getQp()).setActiveHandler(activeHandler);
1067       OutlineHandler oh = ohi.next();
1068       oh.writeOutlines(((QParamsQconf) qcL.getQp()).getSnakeQP(), true);
1069       logger.info("\tCreating parameter file " + activeHandler);
1070       ((QParamsQconf) qcL.getQp()).writeOldParams();
1071       activeHandler++;
1072     } while (ohi.hasNext());
1073 
1074   }
1075 
1076   /**
1077    * Perform conversion depending on which file has been loaded.
1078    * 
1079    * <p>Supported conversions:
1080    * QCONF->paQP -- paQP, snQP, stQP, maQP files are generated (if data present in QCONF)
1081    * paQP->QCONF -- paQP, snQP, stQP, maQP are supported
1082    * 
1083    * @throws QuimpException on every error redirected to GUI. This is final method called from
1084    *         caller. All exceptions during conversion are collected and converted here to GUI.
1085    */
1086   public void doConversion() throws QuimpException {
1087     try {
1088       switch (qcL.isFileLoaded()) {
1089         case QParams.NEW_QUIMP:
1090           generateOldDataFiles();
1091           break;
1092         case QParams.QUIMP_11:
1093           Map<Integer, String> ret = QconfLoader.validatePaqp(
1094                   qcL.getQp().getPathasPath().resolve(qcL.getQp().getParamFile().toPath()));
1095           if (!ret.isEmpty()) {
1096             logger.warn("Sanity check returned the following warnings for paQP structure:");
1097             ret.values().stream().forEach(i -> Arrays.asList(i.split(QconfLoader.SEPARATOR))
1098                     .stream().forEach(j -> logger.warn(j)));
1099             logger.warn("FormatConverter will try to convert such file but it may"
1100                     + " lead to invalid QCONF file");
1101           }
1102           generateNewDataFiles();
1103           break;
1104         default:
1105           throw new IllegalArgumentException(
1106                   "QconfLoader returned unknown version of QuimP or error: " + qcL.isFileLoaded());
1107       }
1108     } catch (IOException qe) {
1109       throw new QuimpException(qe);
1110     }
1111   }
1112 
1113   /**
1114    * Return type of loaded file or 0 if not loaded yet.
1115    * 
1116    * @return {@link QconfLoader#QCONF_INVALID} or {@link QParams#NEW_QUIMP},
1117    *         {@link QParams#QUIMP_11}
1118    */
1119   public int isFileLoaded() {
1120     if (qcL == null) {
1121       return QconfLoader.QCONF_INVALID;
1122     } else {
1123       return qcL.isFileLoaded();
1124     }
1125   }
1126 
1127   /*
1128    * (non-Javadoc)
1129    * 
1130    * @see java.lang.Object#toString()
1131    */
1132   @Override
1133   public String toString() {
1134     String ret = "";
1135     switch (qcL.isFileLoaded()) {
1136       case QParams.NEW_QUIMP:
1137         ret = ret.concat("Experiment date: ")
1138                 .concat(((QParamsQconf) qcL.getQp()).getFileVersion().getBuildstamp()).concat("\n");
1139         ret = ret.concat("File version: ")
1140                 .concat(((QParamsQconf) qcL.getQp()).getFileVersion().getVersion()).concat("\n");
1141         ret = ret.concat("Is BOA analysis present? ").concat(" -- ")
1142                 .concat(Boolean.toString(qcL.isBOAPresent())).concat("\n");
1143         ret = ret.concat("Is ECMM analysis present? ").concat(" -- ")
1144                 .concat(Boolean.toString(qcL.isECMMPresent())).concat("\n");
1145         ret = ret.concat("Is ANA analysis present? ").concat(" -- ")
1146                 .concat(Boolean.toString(qcL.isANAPresent())).concat("\n");
1147         ret = ret.concat("Is Q analysis present? ").concat(" -- ")
1148                 .concat(Boolean.toString(qcL.isQPresent())).concat("\n");
1149         ret = ret.concat("Are stats present? ").concat(" -- ")
1150                 .concat(Boolean.toString(qcL.isStatsPresent())).concat("\n");
1151         return ret;
1152       case QParams.QUIMP_11:
1153         ret = ret.concat(qcL.getQp().getFileName()).concat("\n");
1154         ret = ret.concat("Is file ").concat(qcL.getQp().getParamFile().getName())
1155                 .concat(" present? ").concat(" -- ")
1156                 .concat(Boolean.toString(qcL.getQp().getParamFile().exists())).concat("\n");
1157         ret = ret.concat("Is file ").concat(qcL.getQp().getSnakeQP().getName()).concat(" present? ")
1158                 .concat(" -- ").concat(Boolean.toString(qcL.getQp().getSnakeQP().exists()))
1159                 .concat("\n");
1160         ret = ret.concat("Is file ").concat(qcL.getQp().getStatsQP().getName()).concat(" present? ")
1161                 .concat(" -- ").concat(Boolean.toString(qcL.getQp().getStatsQP().exists()))
1162                 .concat("\n");
1163         ret = ret.concat("Is file ").concat(qcL.getQp().getMotilityFile().getName())
1164                 .concat(" present? ").concat(" -- ")
1165                 .concat(Boolean.toString(qcL.getQp().getMotilityFile().exists())).concat("\n");
1166         ret = ret.concat("Is file ").concat(qcL.getQp().getConvexFile().getName())
1167                 .concat(" present? ").concat(" -- ")
1168                 .concat(Boolean.toString(qcL.getQp().getConvexFile().exists())).concat("\n");
1169         ret = ret.concat("Is file ").concat(qcL.getQp().getCoordFile().getName())
1170                 .concat(" present? ").concat(" -- ")
1171                 .concat(Boolean.toString(qcL.getQp().getCoordFile().exists())).concat("\n");
1172         ret = ret.concat("Is file ").concat(qcL.getQp().getOriginFile().getName())
1173                 .concat(" present? ").concat(" -- ")
1174                 .concat(Boolean.toString(qcL.getQp().getOriginFile().exists())).concat("\n");
1175         ret = ret.concat("Is file ").concat(qcL.getQp().getxmapFile().getName())
1176                 .concat(" present? ").concat(" -- ")
1177                 .concat(Boolean.toString(qcL.getQp().getxmapFile().exists())).concat("\n");
1178         ret = ret.concat("Is file ").concat(qcL.getQp().getymapFile().getName())
1179                 .concat(" present? ").concat(" -- ")
1180                 .concat(Boolean.toString(qcL.getQp().getymapFile().exists())).concat("\n");
1181         File[] tmpf = qcL.getQp().getFluFiles();
1182         for (File f : tmpf) {
1183           ret = ret.concat("Is file ").concat(f.getName()).concat(" present? ").concat(" -- ")
1184                   .concat(Boolean.toString(f.exists())).concat("\n");
1185         }
1186         return ret;
1187       case QParams.OLD_QUIMP:
1188         ret = "toString is not supported for this format";
1189         return ret;
1190       default:
1191         return "No file loaded or file damaged";
1192     }
1193   }
1194 
1195 }