View Javadoc
1   package com.github.celldynamics.quimp;
2   
3   import java.awt.BorderLayout;
4   import java.awt.Button;
5   import java.awt.Checkbox;
6   import java.awt.CheckboxMenuItem;
7   import java.awt.Choice;
8   import java.awt.Color;
9   import java.awt.Container;
10  import java.awt.Dimension;
11  import java.awt.FlowLayout;
12  import java.awt.Graphics;
13  import java.awt.GridBagConstraints;
14  import java.awt.GridBagLayout;
15  import java.awt.GridLayout;
16  import java.awt.Label;
17  import java.awt.Menu;
18  import java.awt.MenuBar;
19  import java.awt.MenuItem;
20  import java.awt.Panel;
21  import java.awt.Rectangle;
22  import java.awt.TextArea;
23  import java.awt.event.ActionEvent;
24  import java.awt.event.ActionListener;
25  import java.awt.event.ItemEvent;
26  import java.awt.event.ItemListener;
27  import java.awt.event.MouseAdapter;
28  import java.awt.event.MouseEvent;
29  import java.awt.event.WindowAdapter;
30  import java.awt.event.WindowEvent;
31  import java.io.File;
32  import java.io.FileNotFoundException;
33  import java.io.IOException;
34  import java.net.URI;
35  import java.nio.file.Path;
36  import java.nio.file.Paths;
37  import java.util.ArrayList;
38  import java.util.List;
39  import java.util.Map;
40  import java.util.concurrent.ExecutionException;
41  import java.util.stream.Collectors;
42  import java.util.stream.IntStream;
43  
44  import javax.swing.BoxLayout;
45  import javax.swing.JOptionPane;
46  import javax.swing.JScrollPane;
47  import javax.swing.JSpinner;
48  import javax.swing.SpinnerNumberModel;
49  import javax.swing.SwingWorker;
50  import javax.swing.event.ChangeEvent;
51  import javax.swing.event.ChangeListener;
52  
53  import org.scijava.vecmath.Point2d;
54  import org.scijava.vecmath.Vector2d;
55  import org.slf4j.Logger;
56  import org.slf4j.LoggerFactory;
57  
58  import com.github.celldynamics.quimp.BOAState.BOAp;
59  import com.github.celldynamics.quimp.BOAState.SegParam;
60  import com.github.celldynamics.quimp.QuimpException.MessageSinkTypes;
61  import com.github.celldynamics.quimp.SnakePluginList.Plugin;
62  import com.github.celldynamics.quimp.filesystem.DataContainer;
63  import com.github.celldynamics.quimp.filesystem.DataContainerInstanceCreator;
64  import com.github.celldynamics.quimp.filesystem.FileDialogEx;
65  import com.github.celldynamics.quimp.filesystem.FileExtensions;
66  import com.github.celldynamics.quimp.filesystem.StatsCollection;
67  import com.github.celldynamics.quimp.filesystem.versions.Converter170202;
68  import com.github.celldynamics.quimp.geom.ExtendedVector2d;
69  import com.github.celldynamics.quimp.plugin.IQuimpCorePlugin;
70  import com.github.celldynamics.quimp.plugin.IQuimpPluginAttachImage;
71  import com.github.celldynamics.quimp.plugin.QuimpPluginException;
72  import com.github.celldynamics.quimp.plugin.binaryseg.BinarySegmentation_;
73  import com.github.celldynamics.quimp.plugin.engine.PluginFactory;
74  import com.github.celldynamics.quimp.plugin.engine.PluginProperties;
75  import com.github.celldynamics.quimp.plugin.snakes.IQuimpBOAPoint2dFilter;
76  import com.github.celldynamics.quimp.plugin.snakes.IQuimpBOASnakeFilter;
77  import com.github.celldynamics.quimp.plugin.utils.QuimpDataConverter;
78  import com.github.celldynamics.quimp.registration.Registration;
79  import com.github.celldynamics.quimp.utils.QuimPArrayUtils;
80  import com.github.celldynamics.quimp.utils.QuimpToolsCollection;
81  import com.github.celldynamics.quimp.utils.graphics.GraphicsElements;
82  import com.google.gson.JsonSyntaxException;
83  
84  import ij.IJ;
85  import ij.ImagePlus;
86  import ij.ImageStack;
87  import ij.WindowManager;
88  import ij.gui.GenericDialog;
89  import ij.gui.ImageCanvas;
90  import ij.gui.NewImage;
91  import ij.gui.Overlay;
92  import ij.gui.PointRoi;
93  import ij.gui.PolygonRoi;
94  import ij.gui.Roi;
95  import ij.gui.StackWindow;
96  import ij.gui.TextRoi;
97  import ij.gui.Toolbar;
98  import ij.gui.YesNoCancelDialog;
99  import ij.io.OpenDialog;
100 import ij.io.SaveDialog;
101 import ij.plugin.PlugIn;
102 import ij.plugin.frame.RoiManager;
103 import ij.process.Blitter;
104 import ij.process.FloatPolygon;
105 import ij.process.ImageConverter;
106 import ij.process.ImageProcessor;
107 import ij.process.StackConverter;
108 
109 /**
110  * Main class implementing BOA plugin.
111  * 
112  * @author Richard Tyson
113  * @author Till Bretschneider
114  * @author Piotr Baniukiewicz
115  */
116 public class BOA_ implements PlugIn {
117   private static final Logger LOGGER = LoggerFactory.getLogger(BOA_.class.getName());
118 
119   /**
120    * Indicate that {@link com.github.celldynamics.quimp.BOA_#runBoa(int, int)} is active.
121    * 
122    * <p>This method calls {@link com.github.celldynamics.quimp.ImageGroup#setIpSliceAll(int)} that
123    * raises event
124    * {@link com.github.celldynamics.quimp.BOA_.CustomStackWindow#updateSliceSelector()} which then
125    * fire other methods.
126    */
127   boolean isSegRunning = false;
128 
129   /**
130    * Used for breaking segmentation if Cancel is hit. If true segmentation is stopped.
131    * 
132    * @see #runBoa(int, int)
133    */
134   private boolean isSegBreakHit = false;
135 
136   /**
137    * The canvas.
138    */
139   CustomCanvas canvas;
140 
141   /**
142    * The window.
143    */
144   CustomStackWindow window;
145 
146   /**
147    * The log area.
148    */
149   static TextArea logArea;
150 
151   /**
152    * Indicate if the BOA plugin is run.
153    */
154   static boolean isBoaRunning = false;
155 
156   /**
157    * The image group.
158    */
159   ImageGroup imageGroup;
160   private Constrictor constrictor;
161   private PluginFactory pluginFactory; // load and maintain plugins
162   /**
163    * Last selection tool selected in IJ.
164    * 
165    * <p>remember last tool to reselect it after truncating or
166    * deleting operation
167    */
168   private String lastTool;
169 
170   /**
171    * Reserved word that stands for plugin that is not selected.
172    */
173   public static final String NONE = "NONE";
174   /**
175    * Reserved word that states full view zoom in zoom choice. Also default text that appears there
176    */
177   private static final String fullZoom = "Frame zoom";
178   /**
179    * Hold current BOA object and provide access to only selected methods from plugin.
180    * 
181    * <p>Reference to this field is passed to plugins and give them possibility to call selected
182    * methods from BOA class
183    */
184   public static ViewUpdater viewUpdater;
185   /**
186    * Keep data from getQuimPBuildInfo().
187    * 
188    * <p>These information are used in About dialog, window title bar, logging, etc. Static because
189    * window related staff is in another classes.
190    */
191   public static QuimpVersion quimpInfo;
192   private static int logCount; // add counter to logged messages
193   /**
194    * Number of Snake plugins available.
195    */
196   public static final int NUM_SNAKE_PLUGINS = 3;
197   // private HistoryLogger historyLogger; // logger
198   /**
199    * Configuration object, available from all modules.
200    * 
201    * <p>Must be initialised here <b>AND</b> in constructor (to reset settings on next BOA call
202    * without quitting Fiji) Keep data that will be serialized.
203    */
204   public static BOAStateBOAState.html#BOAState">BOAState qState = new BOAState(null); // current state of BOA module
205 
206   /**
207    * Main constructor.
208    * 
209    * <p>All static resources should be re-initialized here, otherwise they persist in memory between
210    * subsequent BOA calls from Fiji.
211    */
212   public BOA_() {
213     LOGGER.trace("Starting BOA from default constructor");
214     qState = new BOAState(null);
215     logCount = 1; // reset log count (it is also static)
216   }
217 
218   /**
219    * Main method called from Fiji. Initialises internal BOA structures.
220    * 
221    * @param arg Currently it can be string pointing to plugins directory
222    * @see #setup(ImagePlus)
223    */
224   @Override
225   public void run(final String arg) {
226     if (IJ.versionLessThan("1.45")) {
227       return;
228     }
229 
230     if (BOA_.isBoaRunning) {
231       BOA_.isBoaRunning = false;
232       IJ.error("Warning: Only have one instance of BOA running at a time");
233       return;
234     }
235     // assign current object to ViewUpdater
236     viewUpdater = new ViewUpdater(this);
237     // collect information about quimp version read from jar
238     quimpInfo = QuimP.TOOL_VERSION;
239     // create history logger
240     // historyLogger = new HistoryLogger();
241 
242     // Build plugin engine
243     try {
244       String path;
245       if (QuimP.PLUGIN_DIR == null) {
246         path = IJ.getDirectory("plugins");
247       } else {
248         path = QuimP.PLUGIN_DIR;
249       }
250       if (path == null) {
251         IJ.log("BOA: Plugin directory not found, use provided with arg: " + arg);
252         LOGGER.debug("BOA: Plugin directory not found, use provided with arg: " + arg);
253         path = arg;
254       }
255       // initialize plugin factory (jar scanning and registering)
256       pluginFactory = PluginFactoryFactory.getPluginFactory(path);
257     } catch (Exception e) {
258       IJ.error("Error during initialisation of plugin engine", e.getMessage());
259       LOGGER.debug(e.getMessage(), e);
260       return;
261     }
262 
263     ImagePlus ip = WindowManager.getCurrentImage();
264     // Initialise arrays for plugins instances and give them initial values (GUI)
265     qState = new BOAState(ip, pluginFactory, viewUpdater); // create BOA state machine
266     if (IJ.getVersion().compareTo("1.46") < 0) {
267       qState.boap.useSubPixel = false;
268     } else {
269       qState.boap.useSubPixel = true;
270     }
271 
272     lastTool = IJ.getToolName();
273     // stack or single image?
274     if (ip == null || ip.getNChannels() > 1) {
275       IJ.error("Single channel image required");
276       return;
277     } else if (ip.getStackSize() == 1) {
278       qState.boap.singleImage = true;
279     } else {
280       qState.boap.singleImage = false;
281     }
282     // check if 8-bit image
283     if (ip.getType() != ImagePlus.GRAY8) {
284       YesNoCancelDialog ync =
285               new YesNoCancelDialog(window, "Image bit depth", "8-bit Image required. Convert?");
286       if (ync.yesPressed()) {
287         if (qState.boap.singleImage) {
288           new ImageConverter(ip).convertToGray8();
289         } else {
290           new StackConverter(ip).convertToGray8();
291         }
292       } else {
293         return;
294       }
295     }
296     BOA_.isBoaRunning = true;
297     setup(ip); // create main objects in BOA and BOAState, build window + registration window
298 
299     if (qState.boap.useSubPixel == false) {
300       BOA_.log("Upgrade to ImageJ 1.46, or higher," + "\nto get sub-pixel editing.");
301     }
302     if (IJ.getVersion().compareTo("1.49a") > 0) {
303       BOA_.log("(ImageJ " + IJ.getVersion() + " untested)");
304     }
305 
306     try {
307       if (!qState.nest.isVacant()) {
308         runBoa(1, 1);
309       }
310     } catch (BoaException be) {
311       be.setMessageSinkType(MessageSinkTypes.GUI);
312       be.handleException(IJ.getInstance(), "Inital preview failed");
313     }
314   }
315 
316   /**
317    * Build all BOA windows and setup initial parameters for segmentation Define also
318    * windowListener for cleaning after closing the main window by user.
319    * 
320    * @param ip Reference to image to be processed by BOA
321    * @see BOAp
322    */
323   void setup(final ImagePlus ip) {
324     if (qState.boap.paramsExist == null) {
325       qState.segParam.setDefaults();
326     }
327     qState.boap.setup(ip);
328 
329     qState.nest = new Nest();
330     imageGroup = new ImageGroup(ip, qState.nest);
331     qState.boap.frame = 1;
332     // build window and set its title
333     canvas = new CustomCanvas(imageGroup.getOrgIpl());
334     window = new CustomStackWindow(imageGroup.getOrgIpl(), canvas);
335     window.buildWindow();
336     window.setTitle(window.getTitle() + " :QuimP: " + quimpInfo.getVersion());
337     // validate registered user
338     new Registration(window, "QuimP Registration");
339     // warn about scale - if it was adjusted in BOAState constructor
340     if (qState.boap.isScaleAdjusted()) {
341       BOA_.log("WARNING Scale was zero - set to 1");
342     }
343     if (qState.boap.isfIAdjusted()) {
344       BOA_.log("WARNING Frame interval was zero - set to 1");
345     }
346 
347     // adds window listener called on plugin closing
348     window.addWindowListener(new CustomWindowAdapter());
349 
350     setScales(); // ask user for scales and set them
351     updateImageScale();
352     window.setScalesText();
353 
354     // check for ROIs - Use as cells
355     new RoiManager(); // get open ROI manager, or create a new one
356     RoiManager rm = RoiManager.getInstance();
357     if (rm.getRoisAsArray().length != 0) {
358       qState.nest.addHandlers(rm.getRoisAsArray(), 1);
359     } else {
360       BOA_.log("No cells from ROI manager");
361       if (ip.getRoi() != null) {
362         qState.nest.addHandler(ip.getRoi(), 1);
363       } else {
364         BOA_.log("No cells from selection");
365       }
366     }
367     rm.close();
368     ip.killRoi();
369 
370     constrictor = new Constrictor(); // does computations on snakes
371   }
372 
373   /**
374    * Display about information in BOA window. Called from menu bar. Reads also information from all
375    * found plugins.
376    */
377   void about() {
378     AboutDialog/AboutDialog.html#AboutDialog">AboutDialog ad = new AboutDialog(window); // create about dialog with parent 'window'
379     ad.appendLine(QuimpToolsCollection.getFormattedQuimPversion(quimpInfo)); // display template
380     ad.appendLine("List of found plugins:");
381     ad.appendDistance(); // type ----
382     Map<String, PluginProperties> mp = pluginFactory.getRegisterdPlugins();
383     // iterate over set
384     for (Map.Entry<String, PluginProperties> entry : mp.entrySet()) {
385       ad.appendLine("Plugin name: " + entry.getKey());
386       ad.appendLine("   Plugin type: " + entry.getValue().getType());
387       ad.appendLine("   Plugin path: " + entry.getValue().getFile().toString());
388       ad.appendLine("   Plugin vers: " + entry.getValue().getVersion());
389       // about is not stored in PluginProperties class due to optimization of memory
390       ad.appendLine("   About (returned by plugin):");
391       IQuimpCorePlugin tmpinst = pluginFactory.getInstance(entry.getKey());
392       if (tmpinst != null) { // can be null on problem with instance
393         String about = tmpinst.about(); // may return null
394         if (about != null) {
395           ad.appendLine(about);
396         } else {
397           ad.appendLine("Plugin does not provide about note");
398         }
399       }
400       ad.appendDistance();
401     }
402     ad.setVisible(true); // must be after adding content
403   }
404 
405   /**
406    * Append string to log window in BOA plugin.
407    * 
408    * @param s String to display in BOA window
409    */
410   static void log(final String s) {
411     if (logArea == null) {
412       LOGGER.debug("[" + logCount++ + "] " + s + '\n');
413     } else {
414       logArea.append("[" + logCount++ + "] " + s + '\n');
415     }
416   }
417 
418   /**
419    * Redraw current view. Process outlines by all active plugins. Do not run segmentation again
420    * Updates liveSnake. Also disables UI.
421    * 
422    * <p>Strictly related to current view {@link BOAState.BOAp#frame}.
423    */
424   void recalculatePlugins() {
425     LOGGER.trace("BOA: recalculatePlugins called");
426     SnakeHandler sh;
427     if (qState.nest.isVacant()) { // only update screen
428       imageGroup.updateOverlay(qState.boap.frame);
429       return;
430     }
431     imageGroup.updateToFrame(qState.boap.frame);
432     try {
433       for (int s = 0; s < qState.nest.size(); s++) { // for each snake
434         sh = qState.nest.getHandler(s);
435         if (sh.isSnakeHandlerFrozen()) {
436           LOGGER.debug("SnakeHandler " + sh.getID() + " is frozen");
437           continue;
438         }
439         if (qState.boap.frame < sh.getStartFrame()) {
440           continue;
441         }
442         // but if one is on frame iplStack+n and strtFrame is e.g. 1 it may happen that there is
443         // no continuity of this snake between frames. In this case getBackupSnake
444         // returns null. In general QuimP assumes that if there is a cell on frame iplStack, it
445         // will exist on all consecutive frames.
446         Snake snake = sh.getBackupSnake(qState.boap.frame); // if exist get its backup copy
447         // (segm)
448         if (snake == null || !snake.alive) {
449           continue;
450         }
451         try {
452           Snake out = iterateOverSnakePlugins(snake); // apply all plugins to snake
453           sh.storeThisSnake(out, qState.boap.frame); // set processed snake as final
454         } catch (QuimpPluginException qpe) {
455           // must be rewritten with whole runBOA #65 #67
456           qpe.setMessageSinkType(MessageSinkTypes.IJERROR);
457           BOA_.log(qpe.handleException(null, "Error in filter module"));
458           sh.storeLiveSnake(qState.boap.frame); // so store only segmented snake as final
459         } catch (BoaException be) { // less than 3 nodes in snake
460           be.setMessageSinkType(MessageSinkTypes.IJERROR);
461           BOA_.log(be.handleException(null, "Defective snake returned"));
462         }
463       }
464     } catch (Exception e) {
465       IJ.error("Plugin error", "Output snake may be defective. Reason: " + e.getMessage());
466       LOGGER.debug(e.getMessage(), e);
467     } finally {
468       // historyLogger.addEntry("Plugin settings", qState);
469       qState.store(qState.boap.frame); // always remember state of the BOA that is
470     }
471     imageGroup.updateOverlay(qState.boap.frame);
472   }
473 
474   /**
475    * Override action performed on window closing. Clear BOA._running static variable and prevent
476    * to notify user that QuimP is running when it has been closed and called again.
477    * 
478    * <p>When user closes window by system button QuimP does not ask for saving current work. This is
479    * because by default QuimP window is managed by ImageJ and it probably only hides it on closing
480    * 
481    * <p>This class could be located directly in CustomStackWindow which is included in BOA_. But it
482    * needs to have access to BOA field <tt>running</tt>.
483    * 
484    * @author p.baniukiewicz
485    */
486   class CustomWindowAdapter extends WindowAdapter {
487 
488     /*
489      * (non-Javadoc)
490      * 
491      * @see java.awt.event.WindowAdapter#windowClosed(java.awt.event.WindowEvent)
492      */
493     @Override
494     // This method will be called when BOA_ window is closed already
495     // It is too late for asking user
496     public void windowClosed(final WindowEvent arg0) {
497       LOGGER.trace("CLOSED");
498       BOA_.isBoaRunning = false; // set marker
499       qState.snakePluginList.clear(); // close all opened plugin windows
500       if (qState.binarySegmentationPlugin != null) {
501         qState.binarySegmentationPlugin.showUi(false);
502       }
503       canvas = null; // clear window data
504       imageGroup = null;
505       window = null;
506       // clear static
507       qState = null;
508       viewUpdater = null;
509     }
510 
511     /*
512      * (non-Javadoc)
513      * 
514      * @see java.awt.event.WindowAdapter#windowClosing(java.awt.event.WindowEvent)
515      */
516     @Override
517     public void windowClosing(final WindowEvent arg0) {
518       LOGGER.trace("CLOSING");
519     }
520 
521     /*
522      * (non-Javadoc)
523      * 
524      * @see java.awt.event.WindowAdapter#windowActivated(java.awt.event.WindowEvent)
525      */
526     @Override
527     public void windowActivated(final WindowEvent e) {
528       LOGGER.trace("ACTIVATED");
529       // rebuild menu for this local window
530       // workaround for Mac and theirs menus on top screen bar
531       // IJ is doing the same for activation of its window so every time one has correct menu
532       // on top
533       window.setMenuBar(window.menuBar);
534     }
535   }
536 
537   /**
538    * Supports mouse actions on image at QuimP window according to selected option.
539    * 
540    * @author rtyson
541    *
542    */
543   @SuppressWarnings("serial")
544   class CustomCanvas extends ImageCanvas {
545 
546     /**
547      * Empty constructor.
548      * 
549      * @param imp Reference to image loaded by BOA
550      */
551     CustomCanvas(final ImagePlus imp) {
552       super(imp);
553     }
554 
555     /**
556      * @deprecated Actually not used in this version of QuimP.
557      */
558     @Override
559     public void paint(final Graphics g) {
560       super.paint(g);
561       // int size = 80;
562       // int screenSize = (int)(size*getMagnification());
563       // int x = screenX(imageWidth/2 - size/2);
564       // int y = screenY(imageHeight/2 - size/2);
565       // g.setColor(Color.red);
566       // g.drawOval(x, y, screenSize, screenSize);
567     }
568 
569     /**
570      * Implement mouse action on image loaded to BOA Used for manual editions of segmented
571      * shape. Define reactions of mouse buttons according to GUI state, set by \b Delete and \b
572      * Edit buttons.
573      * 
574      * @see BOAp
575      * @see CustomStackWindow
576      */
577     @Override
578     public void mousePressed(final MouseEvent e) {
579       super.mousePressed(e);
580       if (qState.boap.doDelete) {
581         // BOA_.log("Delete at:
582         // ("+offScreenX(e.getX())+","+offScreenY(e.getY())+")");
583         deleteCell(offScreenX(e.getX()), offScreenY(e.getY()), qState.boap.frame);
584         IJ.setTool(lastTool);
585       }
586       if (qState.boap.doFreeze) {
587         freezeCell(offScreenX(e.getX()), offScreenY(e.getY()), qState.boap.frame);
588       }
589       if (qState.boap.doDeleteSeg) {
590         // BOA_.log("Delete at:
591         // ("+offScreenX(e.getX())+","+offScreenY(e.getY())+")");
592         deleteSegmentation(offScreenX(e.getX()), offScreenY(e.getY()), qState.boap.frame);
593       }
594       if (qState.boap.editMode && qState.boap.editingID == -1) {
595         // BOA_.log("Delete at:
596         // ("+offScreenX(e.getX())+","+offScreenY(e.getY())+")");
597         editSeg(offScreenX(e.getX()), offScreenY(e.getY()), qState.boap.frame);
598       }
599     }
600   } // end of CustomCanvas
601 
602   /**
603    * Extends standard ImageJ StackWindow adding own GUI elements.
604    * 
605    * <p>This class stands for definition of main BOA plugin GUI window. Current state of BOA plugin
606    * is stored at {@link com.github.celldynamics.quimp.BOAState.BOAp} class.
607    * 
608    * @author rtyson
609    * @see BOAp
610    */
611   @SuppressWarnings("serial")
612   class CustomStackWindow extends StackWindow
613           implements ActionListener, ItemListener, ChangeListener {
614 
615     /**
616      * The Constant DEFAULT_SPINNER_SIZE.
617      */
618     static final int DEFAULT_SPINNER_SIZE = 5;
619 
620     /**
621      * Number of currently supported plugins.
622      */
623     static final int SNAKE_PLUGIN_NUM = 3;
624     /**
625      * Any worker that run thread for boa or plugins will be referenced here.
626      * 
627      * @see #runBoaThread(int, int, boolean)
628      * @see #populatePlugins(List)
629      */
630     private SwingWorker<Boolean, Object> sww = null;
631     /**
632      * Block rerun of runBoa() when spinners have been changed programmatically.
633      * 
634      * <p>Modification of spinners from code causes that stateChanged() event is called.
635      */
636     private boolean supressStateChangeBOArun = false;
637     private Button bnSeg; // also play role of Cancel button
638     private Button bnFinish;
639     private Button bnLoad;
640     private Button bnEdit;
641     private Button bnQuit;
642     private Button bnDefault;
643     private Button bnScale;
644     private Button bnCopyLast;
645     private Button bnSave;
646 
647     private Button bnAdd;
648     private Button bnDel;
649     private Button bnDelSeg;
650     private Button bnFreezeCell;
651 
652     private Checkbox cbPrevSnake;
653     private Checkbox cbExpSnake;
654     private Checkbox cbContractingDirection;
655     private Checkbox cbPath;
656     private Choice chZoom;
657 
658     /**
659      * The log panel.
660      */
661     JScrollPane logPanel;
662 
663     private Label fpsLabel;
664     private Label pixelLabel;
665     private Label frameLabel;
666 
667     private JSpinner dsNodeRes;
668     private JSpinner dsVelCrit;
669     private JSpinner dsFImage;
670     private JSpinner dsFCentral;
671     private JSpinner dsFContract;
672     private JSpinner dsFinalShrink;
673 
674     private JSpinner isMaxIterations;
675     private JSpinner isBlowup;
676     private JSpinner isSampletan;
677     private JSpinner isSamplenorm;
678     private Choice chFirstPluginName;
679     private Choice chSecondPluginName;
680     private Choice chThirdPluginName;
681     private Button bnFirstPluginGUI;
682     private Button bnSecondPluginGUI;
683     private Button bnThirdPluginGUI;
684     private Checkbox cbFirstPluginActiv;
685     private Checkbox cbSecondPluginActiv;
686     private Checkbox cbThirdPluginActiv;
687     private Button bnPopulatePlugin; // same as menuPopulatePlugin
688     private Button bnCopyLastPlugin;
689 
690     private MenuBar menuBar; // main menu bar
691     private MenuItem menuAbout;
692     private MenuItem menuOpenHelp;
693     private MenuItem menuSaveConfig;
694     private MenuItem menuLoadConfig;
695     private MenuItem menuShowHistory;
696     private MenuItem menuLoad;
697     private MenuItem menuSave;
698     private MenuItem menuSaveAs;
699     private MenuItem menuDeletePlugin;
700     private MenuItem menuApplyPlugin;
701     private MenuItem menuSegmentationRun;
702     private MenuItem menuSegmentationReset; // items
703     private CheckboxMenuItem cbMenuPlotOriginalSnakes;
704     private CheckboxMenuItem cbMenuPlotHead;
705 
706     private MenuItem menuPopulatePlugin;
707     private CheckboxMenuItem cbMenuZoomFreeze;
708 
709     /**
710      * Default constructor.
711      * 
712      * @param imp Image loaded to plugin
713      * @param ic Image canvas
714      */
715     CustomStackWindow(final ImagePlus imp, final ImageCanvas ic) {
716       super(imp, ic);
717 
718     }
719 
720     /**
721      * Enables or disables all UI controls.
722      * 
723      * @param state true for enabled, false for disabled.
724      */
725     public void enableUi(boolean state) {
726       bnSeg.setEnabled(state);
727       bnFinish.setEnabled(state);
728       bnLoad.setEnabled(state);
729       bnEdit.setEnabled(state);
730       bnQuit.setEnabled(state);
731       bnDefault.setEnabled(state);
732       bnScale.setEnabled(state);
733       bnCopyLast.setEnabled(state);
734       bnSave.setEnabled(state);
735 
736       bnAdd.setEnabled(state);
737       bnDel.setEnabled(state);
738       bnDelSeg.setEnabled(state);
739       bnFreezeCell.setEnabled(state);
740 
741       cbPrevSnake.setEnabled(state);
742       cbExpSnake.setEnabled(false); // disabled option
743       cbContractingDirection.setEnabled(state);
744       cbPath.setEnabled(state);
745       chZoom.setEnabled(state);
746 
747       dsNodeRes.setEnabled(state);
748       dsVelCrit.setEnabled(state);
749       dsFImage.setEnabled(state);
750       dsFCentral.setEnabled(state);
751       dsFContract.setEnabled(state);
752       dsFinalShrink.setEnabled(state);
753 
754       isMaxIterations.setEnabled(state);
755       isBlowup.setEnabled(state);
756       isSampletan.setEnabled(state);
757       isSamplenorm.setEnabled(state);
758       chFirstPluginName.setEnabled(state);
759       chSecondPluginName.setEnabled(state);
760       chThirdPluginName.setEnabled(state);
761 
762       if (chFirstPluginName.getSelectedItem() == NONE) {
763         bnFirstPluginGUI.setEnabled(false);
764         cbFirstPluginActiv.setEnabled(false);
765       } else {
766         bnFirstPluginGUI.setEnabled(state);
767         cbFirstPluginActiv.setEnabled(state);
768       }
769       if (chSecondPluginName.getSelectedItem() == NONE) {
770         bnSecondPluginGUI.setEnabled(false);
771         cbSecondPluginActiv.setEnabled(false);
772       } else {
773         bnSecondPluginGUI.setEnabled(state);
774         cbSecondPluginActiv.setEnabled(state);
775       }
776       if (chThirdPluginName.getSelectedItem() == NONE) {
777         bnThirdPluginGUI.setEnabled(false);
778         cbThirdPluginActiv.setEnabled(false);
779       } else {
780         bnThirdPluginGUI.setEnabled(state);
781         cbThirdPluginActiv.setEnabled(state);
782       }
783       bnPopulatePlugin.setEnabled(state); // same as menuPopulatePlugin
784       bnCopyLastPlugin.setEnabled(state);
785       for (int i = 0; i < menuBar.getMenuCount(); i++) {
786         menuBar.getMenu(i).setEnabled(state);
787       }
788     }
789 
790     /**
791      * Similar to {@link #enableUi(boolean)} but always enables cancel button.
792      * 
793      * @param state true for enabled, false for disabled.
794      */
795     public void enableUiInterruptile(boolean state) {
796       enableUi(state);
797       bnSeg.setEnabled(true);
798     }
799 
800     /**
801      * Build user interface.
802      * 
803      * <p>This method is called as first. The interface is built in three steps: Left side of
804      * window (configuration zone) and right side of main window (logs and other info and
805      * buttons) and finally upper menubar
806      * 
807      * @see com.github.celldynamics.quimp.BOA_.CustomStackWindow#updateWindowState()
808      */
809     public void buildWindow() {
810 
811       setLayout(new BorderLayout(10, 3));
812 
813       if (!qState.boap.singleImage) {
814         remove(sliceSelector);
815       }
816       if (!qState.boap.singleImage) {
817         remove(this.getComponent(1)); // remove the play/pause button
818       }
819       Panel cp = buildControlPanel();
820       Panel sp = buildSetupPanel();
821       add(new Label(""), BorderLayout.NORTH);
822       add(cp, BorderLayout.WEST); // add to the left, position 0
823       add(ic, BorderLayout.CENTER);
824       add(sp, BorderLayout.EAST);
825       add(new Label(""), BorderLayout.SOUTH);
826 
827       LOGGER.debug("Menu: " + getMenuBar());
828       menuBar = buildMenu(); // store menu in var to reuse on window activation
829       setMenuBar(menuBar);
830       pack();
831       updateWindowState(); // window logic on start
832     }
833 
834     /**
835      * Build window menu.
836      * 
837      * <p>Menu is local for this window of QuimP and it is stored in \c quimpMenuBar variable. On
838      * every time when QuimP is active, this menu is restored in
839      * com.github.celldynamics.quimp.BOA_.CustomWindowAdapter.windowActivated(WindowEvent) method
840      * This is due to overwriting menu by IJ on Mac (all menus are on top screen bar)
841      * 
842      * @return Reference to menu bar
843      */
844     final MenuBar buildMenu() {
845       Menu menuHelp; // menu About in menubar
846       Menu menuConfig; // menu Config in menubar
847       Menu menuFile; // menu File in menubar
848       Menu menuPlugin; // menu Plugin in menubar
849 
850       menuBar = new MenuBar();
851 
852       menuConfig = new Menu("Preferences");
853       menuHelp = new Menu("Help");
854       menuFile = new Menu("File");
855       menuPlugin = new Menu("Plugin");
856       Menu menuSegmentation; // menu Segmentation in menubar
857       menuSegmentation = new Menu("Segmentation");
858 
859       // build main line
860       menuBar.add(menuFile);
861       menuBar.add(menuConfig);
862       menuBar.add(menuPlugin);
863       menuBar.add(menuSegmentation);
864       menuBar.add(menuHelp);
865 
866       // add entries
867       menuLoad = new MenuItem("Load experiment");
868       menuLoad.addActionListener(this);
869       menuFile.add(menuLoad);
870       menuSave = new MenuItem("Save experiment");
871       menuSave.addActionListener(this);
872       menuFile.add(menuSave);
873       menuSaveAs = new MenuItem("Save experiment as..");
874       menuSaveAs.addActionListener(this);
875       menuFile.add(menuSaveAs);
876 
877       menuFile.addSeparator();
878       menuLoadConfig = new MenuItem("Load plugin preferences");
879       menuLoadConfig.addActionListener(this);
880       menuFile.add(menuLoadConfig);
881       menuSaveConfig = new MenuItem("Save plugin preferences");
882       menuSaveConfig.addActionListener(this);
883       menuFile.add(menuSaveConfig);
884 
885       menuOpenHelp = new MenuItem("Help Contents");
886       menuOpenHelp.addActionListener(this);
887       menuHelp.add(menuOpenHelp);
888       menuAbout = new MenuItem("About");
889       menuAbout.addActionListener(this);
890       menuHelp.add(menuAbout);
891 
892       cbMenuPlotOriginalSnakes = new CheckboxMenuItem("Plot original");
893       cbMenuPlotOriginalSnakes.setState(qState.boap.isProcessedSnakePlotted);
894       cbMenuPlotOriginalSnakes.addItemListener(this);
895       menuConfig.add(cbMenuPlotOriginalSnakes);
896       cbMenuPlotHead = new CheckboxMenuItem("Plot head");
897       cbMenuPlotHead.setState(qState.boap.isHeadPlotted);
898       cbMenuPlotHead.addItemListener(this);
899       menuConfig.add(cbMenuPlotHead);
900       cbMenuZoomFreeze = new CheckboxMenuItem("Zoom freezes");
901       cbMenuZoomFreeze.setState(qState.boap.isZoomFreeze);
902       cbMenuZoomFreeze.addItemListener(this);
903       menuConfig.add(cbMenuZoomFreeze);
904 
905       menuShowHistory = new MenuItem("Show history");
906       menuShowHistory.addActionListener(this);
907       menuConfig.add(menuShowHistory);
908 
909       menuDeletePlugin = new MenuItem("Discard all");
910       menuDeletePlugin.addActionListener(this);
911       menuPlugin.add(menuDeletePlugin);
912       menuApplyPlugin = new MenuItem("Re-apply all");
913       menuApplyPlugin.addActionListener(this);
914       menuPlugin.add(menuApplyPlugin);
915       menuPopulatePlugin = new MenuItem("Populate to all frames");
916       menuPopulatePlugin.addActionListener(this);
917       menuPlugin.add(menuPopulatePlugin);
918 
919       menuSegmentationRun = new MenuItem("Binary segmentation");
920       menuSegmentationRun.addActionListener(this);
921       menuSegmentationReset = new MenuItem("Clear all");
922       menuSegmentationReset.addActionListener(this);
923       menuSegmentation.add(menuSegmentationRun);
924       menuSegmentation.add(menuSegmentationReset);
925 
926       return menuBar;
927     }
928 
929     /**
930      * Build right side of main BOA window.
931      * 
932      * @return Reference to panel
933      */
934     final Panel buildSetupPanel() {
935       Panel setupPanel = new Panel(); // Main panel comprised from North, Centre and South subpanels
936       Panel northPanel = new Panel(); // Contains static info and four buttons (Scale, Truncate, etc
937       Panel southPanel = new Panel(); // Quit and Finish
938       Panel centerPanel = new Panel();
939       Panel pluginPanelButtons = new Panel(); // buttons below plugins
940 
941       setupPanel.setLayout(new BorderLayout());
942       northPanel.setLayout(new GridLayout(4, 2));
943       southPanel.setLayout(new GridLayout(2, 2));
944       centerPanel.setLayout(new BoxLayout(centerPanel, BoxLayout.PAGE_AXIS));
945 
946       // plugins buttons
947       pluginPanelButtons.setLayout(new GridLayout(1, 2)); // here is number of buttons
948       bnPopulatePlugin = addButton("Populate fwd", pluginPanelButtons);
949       bnCopyLastPlugin = addButton("Copy prev", pluginPanelButtons);
950 
951       // Grid bag for plugin zone
952       GridBagLayout gridbag = new GridBagLayout();
953       GridBagConstraints c = new GridBagConstraints();
954       c.weightx = 0.5;
955       c.fill = GridBagConstraints.HORIZONTAL;
956       c.anchor = GridBagConstraints.LINE_START;
957       Panel pluginPanel = new Panel();
958       pluginPanel.setLayout(gridbag);
959 
960       fpsLabel = new Label("F Interval: " + IJ.d2s(qState.boap.getImageFrameInterval(), 3) + " s");
961       northPanel.add(fpsLabel);
962       pixelLabel = new Label("Scale: " + IJ.d2s(qState.boap.getImageScale(), 6) + " \u00B5m");
963       northPanel.add(pixelLabel);
964 
965       bnScale = addButton("Set Scale", northPanel);
966       bnDelSeg = addButton("Truncate Seg", northPanel);
967       bnAdd = addButton("Add cell", northPanel);
968       bnDel = addButton("Delete cell", northPanel);
969       bnFreezeCell = addButton("Freeze", northPanel);
970 
971       // build subpanel with plugins
972       // get plugins names collected by PluginFactory
973       ArrayList<String> pluginList =
974               qState.snakePluginList.getPluginNames(IQuimpCorePlugin.DOES_SNAKES);
975       // add NONE to list
976       pluginList.add(0, NONE);
977 
978       // plugins are recognized by their names returned from pluginFactory.getPluginNames() so
979       // if there is no names, it is not possible to call nonexisting plugins, because calls
980       // are made using plugin names. see actionPerformed. If plugin of given name (NONE) is
981       // not found getInstance return null which is stored in SnakePluginList and checked
982       // during run
983       // default values for plugin activity are stored in SnakePluginList
984       chFirstPluginName = addComboBox(pluginList.toArray(new String[0]), pluginPanel);
985       c.gridx = 0;
986       c.gridy = 0;
987       pluginPanel.add(chFirstPluginName, c);
988       bnFirstPluginGUI = addButton("GUI", pluginPanel);
989       c.gridx = 1;
990       c.gridy = 0;
991       pluginPanel.add(bnFirstPluginGUI, c);
992       cbFirstPluginActiv = addCheckbox("A", pluginPanel, qState.snakePluginList.isActive(0));
993       c.gridx = 2;
994       c.gridy = 0;
995       pluginPanel.add(cbFirstPluginActiv, c);
996 
997       chSecondPluginName = addComboBox(pluginList.toArray(new String[0]), pluginPanel);
998       c.gridx = 0;
999       c.gridy = 1;
1000       pluginPanel.add(chSecondPluginName, c);
1001       bnSecondPluginGUI = addButton("GUI", pluginPanel);
1002       c.gridx = 1;
1003       c.gridy = 1;
1004       pluginPanel.add(bnSecondPluginGUI, c);
1005       cbSecondPluginActiv = addCheckbox("A", pluginPanel, qState.snakePluginList.isActive(1));
1006       c.gridx = 2;
1007       c.gridy = 1;
1008       pluginPanel.add(cbSecondPluginActiv, c);
1009 
1010       chThirdPluginName = addComboBox(pluginList.toArray(new String[0]), pluginPanel);
1011       c.gridx = 0;
1012       c.gridy = 2;
1013       pluginPanel.add(chThirdPluginName, c);
1014       bnThirdPluginGUI = addButton("GUI", pluginPanel);
1015       c.gridx = 1;
1016       c.gridy = 2;
1017       pluginPanel.add(bnThirdPluginGUI, c);
1018       cbThirdPluginActiv = addCheckbox("A", pluginPanel, qState.snakePluginList.isActive(2));
1019       c.gridx = 2;
1020       c.gridy = 2;
1021       pluginPanel.add(cbThirdPluginActiv, c);
1022 
1023       c.gridx = 0;
1024       c.gridy = 3;
1025       c.gridwidth = 3;
1026       c.fill = GridBagConstraints.HORIZONTAL;
1027       pluginPanel.add(pluginPanelButtons, c);
1028 
1029       // --------build log---------
1030       Panel tp = new Panel(); // panel with text area
1031       tp.setLayout(new GridLayout(1, 1));
1032       logArea = new TextArea(15, 15);
1033       logArea.setEditable(false);
1034       tp.add(logArea);
1035       logPanel = new JScrollPane(tp);
1036 
1037       // ------------------------------
1038 
1039       // --------build south--------------
1040       southPanel.add(new Label("")); // blankes
1041       southPanel.add(new Label("")); // blankes
1042       bnQuit = addButton("Quit", southPanel);
1043       bnFinish = addButton("Save & Quit", southPanel);
1044       // ------------------------------
1045 
1046       centerPanel.add(new Label("Snake Plugins:"));
1047       centerPanel.add(pluginPanel);
1048       centerPanel.add(new Label("Logs:"));
1049       centerPanel.add(logPanel);
1050       setupPanel.add(northPanel, BorderLayout.PAGE_START);
1051       setupPanel.add(centerPanel, BorderLayout.CENTER);
1052       setupPanel.add(southPanel, BorderLayout.PAGE_END);
1053 
1054       if (pluginList.isEmpty()) {
1055         BOA_.log("No plugins found");
1056       } else {
1057         BOA_.log("Found " + (pluginList.size() - 1) + " plugins (see About)");
1058       }
1059 
1060       return setupPanel;
1061     }
1062 
1063     /**
1064      * Build left side of main BOA window.
1065      * 
1066      * @return Reference to built panel
1067      */
1068     final Panel buildControlPanel() {
1069       Panel controlPanel = new Panel();
1070       Panel topPanel = new Panel();
1071       Panel paramPanel = new Panel();
1072       Panel bottomPanel = new Panel();
1073 
1074       controlPanel.setLayout(new BorderLayout());
1075       topPanel.setLayout(new GridLayout(2, 2));
1076       paramPanel.setLayout(new GridLayout(15, 1));
1077       bottomPanel.setLayout(new GridLayout(1, 2));
1078 
1079       // --------build topPanel--------
1080       bnLoad = addButton("Load", topPanel);
1081       bnSave = addButton("Save", topPanel);
1082       bnCopyLast = addButton("Copy prev", topPanel);
1083       bnDefault = addButton("Default", topPanel);
1084 
1085       // -----------------------
1086 
1087       // --------build paramPanel--------------
1088       dsNodeRes = addDoubleSpinner("Node Spacing:", paramPanel, qState.segParam.getNodeRes(), 1.,
1089               20., 0.2, CustomStackWindow.DEFAULT_SPINNER_SIZE);
1090       isMaxIterations = addIntSpinner("Max Iterations:", paramPanel, qState.segParam.max_iterations,
1091               100, 10000, 100, CustomStackWindow.DEFAULT_SPINNER_SIZE);
1092       isBlowup = addIntSpinner("Blowup:", paramPanel, qState.segParam.blowup, -200, 200, 1,
1093               CustomStackWindow.DEFAULT_SPINNER_SIZE);
1094       dsVelCrit = addDoubleSpinner("Crit velocity:", paramPanel, qState.segParam.vel_crit, -2, 2.,
1095               0.001, CustomStackWindow.DEFAULT_SPINNER_SIZE);
1096       dsFImage = addDoubleSpinner("Image F:", paramPanel, qState.segParam.f_image, -10.0, 10.0,
1097               0.01, CustomStackWindow.DEFAULT_SPINNER_SIZE);
1098       dsFCentral = addDoubleSpinner("Central F:", paramPanel, qState.segParam.f_central, -1, 1,
1099               0.002, CustomStackWindow.DEFAULT_SPINNER_SIZE);
1100       dsFContract = addDoubleSpinner("Contract F:", paramPanel, qState.segParam.f_contract, -1, 1,
1101               0.001, CustomStackWindow.DEFAULT_SPINNER_SIZE);
1102       dsFinalShrink = addDoubleSpinner("Final Shrink:", paramPanel, qState.segParam.finalShrink,
1103               -100, 100, 0.5, CustomStackWindow.DEFAULT_SPINNER_SIZE);
1104       isSampletan = addIntSpinner("Sample tan:", paramPanel, qState.segParam.sample_tan, 1, 30, 1,
1105               CustomStackWindow.DEFAULT_SPINNER_SIZE);
1106       isSamplenorm = addIntSpinner("Sample norm:", paramPanel, qState.segParam.sample_norm, 1, 60,
1107               1, CustomStackWindow.DEFAULT_SPINNER_SIZE);
1108 
1109       cbPrevSnake =
1110               addCheckbox("Use Previous Snake", paramPanel, qState.segParam.use_previous_snake);
1111       cbExpSnake = addCheckbox("Expanding Snake", paramPanel, qState.segParam.expandSnake);
1112       cbExpSnake.setEnabled(false); // FIXME DISABLED OPTION
1113       cbContractingDirection =
1114               addCheckbox("Contracing Snake", paramPanel, qState.segParam.contractingDirection);
1115 
1116       Panel segEditPanel = new Panel();
1117       segEditPanel.setLayout(new GridLayout(1, 2));
1118       bnSeg = addButton("SEGMENT", segEditPanel);
1119       bnEdit = addButton("Edit", segEditPanel);
1120       paramPanel.add(segEditPanel);
1121 
1122       // mini panel comprised from slice selector and frame number (if not single image)
1123       Panel sliderPanel = new Panel();
1124       sliderPanel.setLayout(new FlowLayout(FlowLayout.LEFT));
1125 
1126       if (!qState.boap.singleImage) {
1127         sliceSelector.setPreferredSize(new Dimension(165, 20));
1128         sliceSelector.addAdjustmentListener(this);
1129         sliderPanel.add(sliceSelector);
1130         // frame number on right of slice selector
1131         frameLabel = new Label(imageGroup.getOrgIpl().getSlice() + "  ");
1132         sliderPanel.add(frameLabel);
1133       }
1134       paramPanel.add(sliderPanel);
1135       // ----------------------------------
1136 
1137       // -----build bottom panel---------
1138       cbPath = addCheckbox("Show paths", bottomPanel, qState.segParam.showPaths);
1139       chZoom = addComboBox(new String[] { fullZoom }, bottomPanel);
1140       // add mouse listener to create menu dynamically on click
1141       chZoom.addMouseListener(new MouseAdapter() {
1142         @Override
1143         public void mousePressed(MouseEvent e) {
1144           LOGGER.trace("EVENT:mousePressed");
1145           fillZoomChoice();
1146         }
1147       });
1148       // -------------------------------
1149       // build control panel
1150 
1151       controlPanel.add(topPanel, BorderLayout.NORTH);
1152       controlPanel.add(paramPanel, BorderLayout.CENTER);
1153       controlPanel.add(bottomPanel, BorderLayout.SOUTH);
1154 
1155       return controlPanel;
1156     }
1157 
1158     /**
1159      * Rebuild Zoom choice UI according to cells on current frame {@link BOAp#frame}.
1160      * 
1161      * <p>According to #193 if there is no cell left it creates empty entry and set it selected to
1162      * enforce user to set explicitly default unzoom value and fire itemStateChanged.
1163      */
1164     private void fillZoomChoice() {
1165       String prev = chZoom.getSelectedItem();
1166       LOGGER.trace(prev);
1167       chZoom.removeAll();
1168       chZoom.add(fullZoom); // default word for full zoom (100% of view)
1169       List<Integer> frames = qState.nest.getSnakesforFrame(qState.boap.frame);
1170       for (Integer i : frames) {
1171         chZoom.add(i.toString());
1172       }
1173       if (chZoom.getItemCount() == 1) { // dirty trick to enforce triggering itemStateChanged (#193)
1174         chZoom.add("");
1175         chZoom.select("");
1176       } else {
1177         chZoom.select(prev); // select last selected (if exists)
1178       }
1179     }
1180 
1181     /**
1182      * Helper method for adding buttons to UI. Creates UI element and adds it to panel
1183      * 
1184      * @param label Label on button
1185      * @param p Reference to the panel where button is located
1186      * @return Reference to created button
1187      */
1188     private Button addButton(final String label, final Container p) {
1189       Button b = new Button(label);
1190       b.addActionListener(this);
1191       p.add(b);
1192       return b;
1193     }
1194 
1195     /**
1196      * Helper method for creating checkbox in UI.
1197      * 
1198      * @param label Label of checkbox
1199      * @param p Reference to the panel where checkbox is located
1200      * @param d Initial state of checkbox
1201      * @return Reference to created checkbox
1202      */
1203     private Checkbox addCheckbox(final String label, final Container p, boolean d) {
1204       Checkbox c = new Checkbox(label, d);
1205       c.addItemListener(this);
1206       p.add(c);
1207       return c;
1208     }
1209 
1210     /**
1211      * Helper method for creating ComboBox in UI. Creates UI element and adds it to panel
1212      *
1213      * @param s Strings to be included in ComboBox
1214      * @param mp Reference to the panel where ComboBox is located
1215      * @return Reference to created ComboBox
1216      */
1217     private Choice addComboBox(final String[] s, final Container mp) {
1218       Choice c = new Choice();
1219       for (String st : s) {
1220         c.add(st);
1221       }
1222       c.select(0);
1223       c.addItemListener(this);
1224       mp.add(c);
1225       return c;
1226     }
1227 
1228     /**
1229      * Helper method for creating spinner in UI with real values.
1230      * 
1231      * @param s Label of spinner (added on its left side)
1232      * @param mp Reference of panel where spinner is located
1233      * @param d The current vale of model
1234      * @param min The first number in sequence
1235      * @param max The last number in sequence
1236      * @param step The difference between numbers in sequence
1237      * @param columns The number of columns preferred for display
1238      * @return Reference to created spinner
1239      */
1240     private JSpinner addDoubleSpinner(final String s, final Container mp, double d, double min,
1241             double max, double step, int columns) {
1242       SpinnerNumberModel model = new SpinnerNumberModel(d, min, max, step);
1243       JSpinner spinner = new JSpinner(model);
1244       ((JSpinner.DefaultEditor) spinner.getEditor()).getTextField().setColumns(columns);
1245       spinner.addChangeListener(this);
1246 
1247       Panel p = new Panel();
1248       p.setLayout(new FlowLayout(FlowLayout.RIGHT));
1249       Label label = new Label(s);
1250       p.add(label);
1251       p.add(spinner);
1252       mp.add(p);
1253       return spinner;
1254     }
1255 
1256     /**
1257      * Helper method for creating spinner in UI with integer values.
1258      * 
1259      * @param s Label of spinner (added on its left side)
1260      * @param mp Reference of panel where spinner is located
1261      * @param d The current vale of model
1262      * @param min The first number in sequence
1263      * @param max The last number in sequence
1264      * @param step The difference between numbers in sequence
1265      * @param columns The number of columns preferred for display
1266      * @return Reference to created spinner
1267      */
1268     private JSpinner addIntSpinner(final String s, final Container mp, int d, int min, int max,
1269             int step, int columns) {
1270       SpinnerNumberModel model = new SpinnerNumberModel(d, min, max, step);
1271       JSpinner spinner = new JSpinner(model);
1272       ((JSpinner.DefaultEditor) spinner.getEditor()).getTextField().setColumns(columns);
1273       spinner.addChangeListener(this);
1274 
1275       Panel p = new Panel();
1276       p.setLayout(new FlowLayout(FlowLayout.RIGHT));
1277       Label label = new Label(s);
1278       p.add(label);
1279       p.add(spinner);
1280       mp.add(p);
1281       return spinner;
1282     }
1283 
1284     /**
1285      * Set default values defined in model class {@link com.github.celldynamics.quimp.BOAState.BOAp}
1286      * and update UI.
1287      * 
1288      * @see BOAp
1289      */
1290     private void setDefualts() {
1291       qState.segParam.setDefaults();
1292       updateSpinnerValues();
1293       cbContractingDirection.setState(qState.segParam.contractingDirection);
1294     }
1295 
1296     /**
1297      * Update spinners in BOA UI Update spinners according to values stored in machine state
1298      * {@link com.github.celldynamics.quimp.BOAState.BOAp}.
1299      * 
1300      * @see BOAp
1301      */
1302     private void updateSpinnerValues() {
1303       // block rerun of runBoa() that is called on Spinner event
1304       supressStateChangeBOArun = true;
1305       dsNodeRes.setValue(qState.segParam.getNodeRes());
1306       dsVelCrit.setValue(qState.segParam.vel_crit);
1307       dsFImage.setValue(qState.segParam.f_image);
1308       dsFCentral.setValue(qState.segParam.f_central);
1309       dsFContract.setValue(qState.segParam.f_contract);
1310       dsFinalShrink.setValue(qState.segParam.finalShrink);
1311       isMaxIterations.setValue(qState.segParam.max_iterations);
1312       isBlowup.setValue(qState.segParam.blowup);
1313       isSampletan.setValue(qState.segParam.sample_tan);
1314       isSamplenorm.setValue(qState.segParam.sample_norm);
1315       supressStateChangeBOArun = false;
1316     }
1317 
1318     /**
1319      * Update checkboxes.
1320      * 
1321      * @see com.github.celldynamics.quimp.SnakePluginList
1322      * @see #itemStateChanged(ItemEvent)
1323      */
1324     private void updateCheckBoxes() {
1325       // first plugin activity
1326       cbFirstPluginActiv.setState(qState.snakePluginList.isActive(0));
1327       // second plugin activity
1328       cbSecondPluginActiv.setState(qState.snakePluginList.isActive(1));
1329       // third plugin activity
1330       cbThirdPluginActiv.setState(qState.snakePluginList.isActive(2));
1331     }
1332 
1333     /**
1334      * Update Menu checkboxes.
1335      */
1336     private void updateMenus() {
1337       cbMenuPlotOriginalSnakes.setState(qState.boap.isProcessedSnakePlotted);
1338       cbMenuPlotHead.setState(qState.boap.isHeadPlotted);
1339       cbMenuZoomFreeze.setState(qState.boap.isZoomFreeze);
1340     }
1341 
1342     /**
1343      * Update static fileds on window.
1344      */
1345     private void updateStatics() {
1346       setScalesText();
1347     }
1348 
1349     /**
1350      * Update Choices.
1351      * 
1352      * <p>This method is called from CustomStackWindow.itemStateChanged(ItemEvent) to update colors
1353      * of Choices.
1354      * 
1355      * @see com.github.celldynamics.quimp.SnakePluginList
1356      * @see #itemStateChanged(ItemEvent)
1357      */
1358     private void updateChoices() {
1359       final Color ok = new Color(178, 255, 102);
1360       final Color bad = new Color(255, 153, 153);
1361       // first slot snake plugin
1362       if (qState.snakePluginList.getName(0).isEmpty()) {
1363         chFirstPluginName.select(NONE);
1364         chFirstPluginName.setBackground(null);
1365       } else {
1366         // try to select name from pluginList in choice
1367         chFirstPluginName.select(qState.snakePluginList.getName(0));
1368         // tried selecting but still on none - it means that plugin name from snkePluginList is not
1369         // on choice list. Tis may happen when choice is propagated from directory
1370         // butsnakePluginList from external QCONF
1371         if (chFirstPluginName.getSelectedItem().equals(NONE)) {
1372           chFirstPluginName.add(qState.snakePluginList.getName(0)); // add to list
1373           chFirstPluginName.setBackground(bad); // set as bad
1374         } else if (qState.snakePluginList.getInstance(0) == null) {
1375           // WARN does not check instance(0) is the instance of getName(0)
1376           chFirstPluginName.setBackground(bad);
1377         } else {
1378           chFirstPluginName.setBackground(ok);
1379         }
1380       }
1381       // second slot snake plugin
1382       if (qState.snakePluginList.getName(1).isEmpty()) {
1383         chSecondPluginName.select(NONE);
1384         chSecondPluginName.setBackground(null);
1385       } else {
1386         chSecondPluginName.select(qState.snakePluginList.getName(1));
1387         if (chSecondPluginName.getSelectedItem().equals(NONE)) {
1388           chSecondPluginName.add(qState.snakePluginList.getName(1)); // add to list
1389           chSecondPluginName.setBackground(bad); // set as bad
1390         } else if (qState.snakePluginList.getInstance(1) == null) {
1391           chSecondPluginName.setBackground(bad);
1392         } else {
1393           chSecondPluginName.setBackground(ok);
1394         }
1395       }
1396       // third slot snake plugin
1397       if (qState.snakePluginList.getName(2).isEmpty()) {
1398         chThirdPluginName.select(NONE);
1399         chThirdPluginName.setBackground(null);
1400       } else {
1401         chThirdPluginName.select(qState.snakePluginList.getName(2));
1402         if (chThirdPluginName.getSelectedItem().equals(NONE)) {
1403           chThirdPluginName.add(qState.snakePluginList.getName(2)); // add to list
1404           chThirdPluginName.setBackground(bad); // set as bad
1405         } else if (qState.snakePluginList.getInstance(2) == null) {
1406           chThirdPluginName.setBackground(bad);
1407         } else {
1408           chThirdPluginName.setBackground(ok);
1409         }
1410       }
1411       // zoom choice
1412       if (qState.boap.snakeToZoom > -1) {
1413         chZoom.select(String.valueOf(qState.boap.snakeToZoom));
1414       }
1415 
1416     }
1417 
1418     /**
1419      * Implement user interface logic.
1420      * 
1421      * <p>Do not refresh values, rather disable/enable controls.
1422      */
1423     private void updateWindowState() {
1424       updateCheckBoxes(); // update checkboxes
1425       updateChoices(); // and choices
1426       updateStatics();
1427 
1428       // Rule 1 - NONE on any slot in filters disable GUI button and Active checkbox but only if
1429       // there is no worker working
1430       if (sww == null || sww.getState() == SwingWorker.StateValue.DONE) {
1431         if (chFirstPluginName.getSelectedItem() == NONE) {
1432           cbFirstPluginActiv.setEnabled(false);
1433           bnFirstPluginGUI.setEnabled(false);
1434         } else {
1435           cbFirstPluginActiv.setEnabled(true);
1436           bnFirstPluginGUI.setEnabled(true);
1437         }
1438         if (chSecondPluginName.getSelectedItem() == NONE) {
1439           cbSecondPluginActiv.setEnabled(false);
1440           bnSecondPluginGUI.setEnabled(false);
1441         } else {
1442           cbSecondPluginActiv.setEnabled(true);
1443           bnSecondPluginGUI.setEnabled(true);
1444         }
1445         if (chThirdPluginName.getSelectedItem() == NONE) {
1446           cbThirdPluginActiv.setEnabled(false);
1447           bnThirdPluginGUI.setEnabled(false);
1448         } else {
1449           cbThirdPluginActiv.setEnabled(true);
1450           bnThirdPluginGUI.setEnabled(true);
1451         }
1452       }
1453 
1454     }
1455 
1456     /**
1457      * Main method that handles all actions performed on UI elements.
1458      * 
1459      * <p>Do not support mouse events, only UI elements like buttons, spinners and menus. Runs also
1460      * main algorithm on specified input state and update screen on plugins operations.
1461      * 
1462      * @param e Type of event
1463      * @see com.github.celldynamics.quimp.BOAState.BOAp
1464      * @see com.github.celldynamics.quimp.BOA_.CustomStackWindow#updateWindowState()
1465      */
1466     @Override
1467     public void actionPerformed(final ActionEvent e) {
1468       LOGGER.trace("EVENT:actionPerformed");
1469       boolean run = false; // some actions require to re-run segmentation. They set it to true
1470       Object b = e.getSource();
1471       if (b == bnDel && !qState.boap.editMode && !qState.boap.doDeleteSeg
1472               && !qState.boap.doFreeze) {
1473         if (qState.boap.doDelete == false) {
1474           bnDel.setLabel("*STOP DEL*");
1475           qState.boap.doDelete = true;
1476           lastTool = IJ.getToolName();
1477           IJ.setTool(Toolbar.LINE);
1478         } else {
1479           qState.boap.doDelete = false;
1480           bnDel.setLabel("Delete cell");
1481           IJ.setTool(lastTool);
1482         }
1483         return;
1484       }
1485       if (qState.boap.doDelete) { // stop if delete is on
1486         BOA_.log("**DELETE IS ON**");
1487         return;
1488       }
1489       if (b == bnFreezeCell && !qState.boap.editMode && !qState.boap.doDeleteSeg
1490               && !qState.boap.doDelete) {
1491         setFreeze(!qState.boap.doFreeze);
1492         return;
1493       }
1494       if (qState.boap.doFreeze) { // stop if delete is on
1495         BOA_.log("**FREEZE IS ON**");
1496         return;
1497       }
1498 
1499       if (b == bnDelSeg && !qState.boap.editMode) {
1500         if (!qState.boap.doDeleteSeg) {
1501           bnDelSeg.setLabel("*STOP TRUNCATE*");
1502           qState.boap.doDeleteSeg = true;
1503           lastTool = IJ.getToolName();
1504           IJ.setTool(Toolbar.LINE);
1505         } else {
1506           qState.boap.doDeleteSeg = false;
1507           bnDelSeg.setLabel("Truncate Seg");
1508           IJ.setTool(lastTool);
1509         }
1510         return;
1511       }
1512       if (qState.boap.doDeleteSeg) { // stop if delete is on
1513         BOA_.log("**TRUNCATE SEG IS ON**");
1514         return;
1515       }
1516       if (b == bnEdit) {
1517         if (qState.boap.editMode == false) {
1518           bnEdit.setLabel("*STOP EDIT*");
1519           BOA_.log("**EDIT IS ON**");
1520           qState.boap.editMode = true;
1521           lastTool = IJ.getToolName();
1522           IJ.setTool(Toolbar.LINE);
1523           if (qState.nest.size() == 1) {
1524             editSeg(0, 0, qState.boap.frame); // if only 1 snake go straight to edit, if
1525           }
1526           // more user must pick one
1527           // remember that this frame is edited
1528           qState.storeOnlyEdited(qState.boap.frame);
1529         } else {
1530           qState.boap.editMode = false;
1531           if (qState.boap.editingID != -1) {
1532             stopEdit();
1533           }
1534           bnEdit.setLabel("Edit");
1535           IJ.setTool(lastTool);
1536         }
1537         return;
1538       }
1539       if (qState.boap.editMode) { // stop if edit on
1540         BOA_.log("**EDIT IS ON**");
1541         return;
1542       }
1543       if (b == bnDefault) { // run in thread
1544         this.setDefualts();
1545         run = true;
1546       } else if (b == bnSeg) { // main segmentation procedure starts here
1547         if (sww != null && sww.getState() != SwingWorker.StateValue.DONE) {
1548           // if any worker works
1549           isSegBreakHit = true;
1550           return;
1551         }
1552         runBoaThread(qState.boap.frame, qState.boap.getFrames(), true);
1553       } else if (b == bnScale) {
1554         setScales();
1555         pixelLabel.setText("Scale: " + IJ.d2s(qState.boap.getImageScale(), 6) + " \u00B5m");
1556         fpsLabel.setText("F Interval: " + IJ.d2s(qState.boap.getImageFrameInterval(), 3) + " s");
1557       } else if (b == bnAdd) {
1558         addCell(canvas.getImage().getRoi(), qState.boap.frame);
1559         canvas.getImage().killRoi();
1560       } else if (b == bnFinish) {
1561         fpsLabel.setName("moo");
1562         finish(true); // ask for new file
1563         quit();
1564       } else if (b == bnQuit) {
1565         YesNoCancelDialog ync;
1566         ync = new YesNoCancelDialog(window, "Quit", "Quit without saving?");
1567         if (!ync.yesPressed()) {
1568           return;
1569         }
1570         quit();
1571       } else if (b == bnSave || b == menuSave) {
1572         finish(false); // update old file
1573       }
1574       if (b == menuSaveAs) {
1575         finish(true); // create new file
1576       }
1577       // process plugin GUI buttons
1578       if (b == bnFirstPluginGUI) {
1579         LOGGER.debug("First plugin GUI, state of BOAp is " + qState.snakePluginList.getInstance(0));
1580         if (qState.snakePluginList.getInstance(0) != null) {
1581           qState.snakePluginList.getInstance(0).showUi(true);
1582         }
1583       }
1584       if (b == bnSecondPluginGUI) {
1585         LOGGER.debug(
1586                 "Second plugin GUI, state of BOAp is " + qState.snakePluginList.getInstance(1));
1587         if (qState.snakePluginList.getInstance(1) != null) {
1588           qState.snakePluginList.getInstance(1).showUi(true);
1589         }
1590       }
1591       if (b == bnThirdPluginGUI) {
1592         LOGGER.debug("Third plugin GUI, state of BOAp is " + qState.snakePluginList.getInstance(2));
1593         if (qState.snakePluginList.getInstance(2) != null) {
1594           qState.snakePluginList.getInstance(2).showUi(true);
1595         }
1596       }
1597       if (b == bnCopyLastPlugin) { // run in thread
1598         int frameCopyFrom = qState.boap.frame - 1;
1599         if (frameCopyFrom < 1 || frameCopyFrom > qState.boap.getFrames()) {
1600           return;
1601         }
1602         LOGGER.debug(
1603                 "Copy config from frame " + frameCopyFrom + " current frame " + qState.boap.frame);
1604         qState.copyPluginListFromSnapshot(frameCopyFrom);
1605         setBusyStatus(true, false);
1606         recalculatePlugins();
1607         setBusyStatus(false, true); // update screen
1608       }
1609 
1610       if (b == bnCopyLast) { // copy previous settings
1611         int frameCopyFrom = qState.boap.frame - 1;
1612         if (frameCopyFrom < 1 || frameCopyFrom > qState.boap.getFrames()) {
1613           return;
1614         }
1615         LOGGER.debug(
1616                 "Copy config from frame " + frameCopyFrom + " current frame " + qState.boap.frame);
1617         qState.copySegParamFromSnapshot(frameCopyFrom);
1618         updateSpinnerValues(); // update segmentation gui
1619         run = true; // re run BOA (+plugins)
1620       }
1621       if (b == bnPopulatePlugin) { // copy plugin stack forward
1622         List<Integer> range = IntStream.rangeClosed(qState.boap.frame + 1, qState.boap.getFrames())
1623                 .boxed().collect(Collectors.toList());
1624         populatePlugins(range);
1625       }
1626       // menu listeners
1627       if (b == menuAbout) {
1628         about();
1629       }
1630       if (b == menuOpenHelp) {
1631         String url = new PropertyReader().readProperty("quimpconfig.properties", "manualURL");
1632         try {
1633           java.awt.Desktop.getDesktop().browse(new URI(url));
1634         } catch (Exception e1) {
1635           IJ.error("Could not open help", e1.getMessage());
1636           LOGGER.debug(e1.getMessage(), e1);
1637         }
1638         return;
1639       }
1640       if (b == menuSaveConfig) {
1641         String saveIn = qState.boap.getOutputFileCore().getParent();
1642         saveIn = (saveIn == null) ? System.getProperty("user.dir") : saveIn;
1643         SaveDialog sd = new SaveDialog("Save plugin config data...", saveIn,
1644                 qState.boap.getFileName(), FileExtensions.pluginFileExt);
1645         if (sd.getFileName() != null) {
1646           try {
1647             // Create Serialization object with extra info layer
1648             Serializer<SnakePluginList> s;
1649             s = new Serializer<>(qState.snakePluginList, quimpInfo);
1650             s.setPretty(); // set pretty format
1651             s.save(sd.getDirectory() + sd.getFileName()); // save it
1652             s = null; // remove
1653           } catch (FileNotFoundException e1) {
1654             IJ.error("Problem with saving plugin config", e1.getMessage());
1655             LOGGER.debug(e1.getMessage(), e1);
1656           }
1657         }
1658       }
1659 
1660       /*
1661        * Loads configuration of current filter stack.
1662        * 
1663        * @see <a href=
1664        * "http://www.trac-wsbc.linkpc.net:8080/trac/QuimP/ticket/155">http://www.trac-wsbc.linkpc.
1665        * net:8080/trac/QuimP/ticket/155</a>
1666        */
1667       if (b == menuLoadConfig) {
1668         OpenDialog od = new OpenDialog("Load plugin config data...", "");
1669         if (od.getFileName() != null) {
1670           try {
1671             Serializer<SnakePluginList> loaded; // loaded instance
1672             // create serializer
1673             Serializer<SnakePluginList> s =
1674                     new Serializer<>(SnakePluginList.class, QuimP.TOOL_VERSION);
1675             s.registerConverter(new Converter170202<>(QuimP.TOOL_VERSION));
1676             // pass data to constructor of serialized object. Those data are not
1677             // serialized and must be passed externally
1678             s.registerInstanceCreator(SnakePluginList.class,
1679                     new SnakePluginListInstanceCreator(3, pluginFactory, viewUpdater));
1680             loaded = s.load(od.getDirectory() + od.getFileName());
1681             // restore loaded objects
1682             qState.snakePluginList.clear(); // closes windows, etc
1683             qState.snakePluginList = loaded.obj; // replace with fresh instance
1684             qState.store(qState.boap.frame); // copy loaded snakePluginList to snapshots
1685             // commented after #155
1686             /*
1687              * YesNoCancelDialog yncd = new YesNoCancelDialog(IJ.getInstance(),
1688              * "Warning", "Would you like to load this configuration for all frames?");
1689              * if (yncd.yesPressed()) { // propagate over all frames for (int i = 0; i <
1690              * qState.snakePluginListSnapshots.size(); i++) { if (i == qState.boap.frame
1691              * - 1) continue; // do not copy itself
1692              * qState.snakePluginListSnapshots.set(i,
1693              * qState.snakePluginList.getDeepCopy()); } }
1694              */
1695             recalculatePlugins(); // update screen
1696           } catch (IOException e1) {
1697             IJ.error("Problem with loading plugin config", e1.getMessage());
1698             LOGGER.debug(e1.getMessage(), e1);
1699           } catch (JsonSyntaxException e1) {
1700             IJ.error("Problem with configuration file", e1.getMessage());
1701             LOGGER.debug(e1.getMessage(), e1);
1702           } catch (Exception e1) {
1703             IJ.error("Error", "File can not be loaded or parsed" + e1.getMessage());
1704             LOGGER.debug(e1.getMessage(), e1);
1705           }
1706         }
1707       }
1708 
1709       /*
1710        * Shows history window.
1711        * 
1712        * When showed all actions are notified there. This may slow down the program
1713        */
1714       if (b == menuShowHistory) {
1715         JOptionPane.showMessageDialog(window,
1716                 "The full history of changes is avaiable after saving your work in the" + " file "
1717                         + FileExtensions.newConfigFileExt);
1718         /*
1719          * if (historyLogger.isOpened()) historyLogger.closeHistory(); else
1720          * historyLogger.openHistory();
1721          */
1722       }
1723 
1724       /**
1725        * Load global config - QCONF file or paQP file. It depends on QuimP.newFileFormat
1726        * 
1727        * Checks also whether the name of the image sealed in config file is the same as those
1728        * opened currently. If not user has an option to break the procedure or continue
1729        * loading.
1730        */
1731       if (b == menuLoad || b == bnLoad) {
1732         FileDialogExfilesystem/FileDialogEx.html#FileDialogEx">FileDialogEx od = new FileDialogEx(IJ.getInstance());
1733         od.setDirectory(OpenDialog.getLastDirectory());
1734         try {
1735           if (QuimP.newFileFormat.get() == true) { // load QCONF
1736             od.setExtension(FileExtensions.newConfigFileExt);
1737             if (od.showOpenDialog() == null) {
1738               return;
1739             }
1740             loadQconfConfiguration(Paths.get(od.getDirectory(), od.getFile()));
1741           }
1742           if (QuimP.newFileFormat.get() == false) { // old paQP and snQP
1743             od.setExtension(FileExtensions.configFileExt);
1744             if (od.showOpenDialog() == null) {
1745               return;
1746             }
1747             if (qState.readParams(od.getPath().toFile())) {
1748               updateSpinnerValues();
1749               if (loadSnakes()) {
1750                 run = false;
1751               } else {
1752                 run = true;
1753               }
1754             }
1755           }
1756         } catch (IOException e1) {
1757           IJ.error("Problem with loading plugin config", e1.getMessage());
1758           LOGGER.debug(e1.getMessage(), e1); // if debug enabled - get more info
1759         } catch (JsonSyntaxException e1) {
1760           IJ.error("Problem with configuration file", e1.getMessage());
1761           LOGGER.debug(e1.getMessage(), e1);
1762         } catch (Exception e1) { // eg json but wrong
1763           IJ.error("Error", "File can not be loaded or parsed" + e1.getMessage());
1764           LOGGER.debug(e1.getMessage(), e1);
1765         }
1766       }
1767 
1768       /*
1769        * Discard all plugins.
1770        * 
1771        * In general it does: reset current snakePluginList and snakePluginListSnapshots,
1772        * Copies segSnakes to finalSnakes
1773        */
1774       if (b == menuDeletePlugin) {
1775         // clear all plugins
1776         qState.snakePluginList.clear();
1777         for (SnakePluginList sp : qState.snakePluginListSnapshots) {
1778           if (sp != null) {
1779             sp.clear();
1780           }
1781         }
1782         // copy snakes to finals
1783         for (int i = 0; i < qState.nest.size(); i++) {
1784           SnakeHandler snakeHandler = qState.nest.getHandler(i);
1785           snakeHandler.copyFromSegToFinal();
1786         }
1787         // update window
1788         imageGroup.updateOverlay(qState.boap.frame);
1789       }
1790 
1791       /*
1792        * Reload and all plugins stored in snakePluginListSnapshot. Note that plugins are not
1793        * executed, only loaded.
1794        * 
1795        * qState.snakePluginList.clear(); can not be called here because
1796        * com.github.celldynamics.quimp.BOAState.restore(int) makes reference to
1797        * snakePluginListSnapshot in snakePluginList. Thus, cleaning snakePluginList deletes
1798        * one entry in snakePluginListSnapshot
1799        */
1800       if (b == menuApplyPlugin) {
1801         // iterate over snapshots and try to restore plugins in snapshots
1802         for (SnakePluginList sp : qState.snakePluginListSnapshots) {
1803           sp.afterSerialize();
1804         }
1805         // copy snapshots for frame to current snakePluginList (and segParams)
1806         qState.restore(qState.boap.frame);
1807         // recalculatePlugins(); // update screen
1808       }
1809 
1810       /*
1811        * Copy current plugin tree to all frames and applies plugins.
1812        */
1813       if (b == menuPopulatePlugin) { // run in thread
1814         List<Integer> range = IntStream.rangeClosed(1, qState.boap.getFrames()).boxed()
1815                 .collect(Collectors.toList());
1816         populatePlugins(range);
1817       }
1818 
1819       /*
1820        * Run segmentation from mask file.
1821        */
1822       if (b == menuSegmentationRun) {
1823         if (qState.binarySegmentationPlugin != null) {
1824           if (!qState.binarySegmentationPlugin.isWindowVisible()) {
1825             qState.binarySegmentationPlugin.showUi(true);
1826           }
1827         } else {
1828           qState.binarySegmentationPlugin = new BinarySegmentation_(); // create instance
1829           qState.binarySegmentationPlugin.attachNest(qState.nest); // attach data
1830           // allow plugin to update screen
1831           qState.binarySegmentationPlugin.attachContext(viewUpdater);
1832           // plugin is run internally after Apply update screen is always on Apply button of plugin
1833           qState.binarySegmentationPlugin.showUi(true);
1834         }
1835         qState.binarySegmentationPlugin.attachImagePlus(imageGroup.getOrgIpl());
1836         // regenerate stored data and create snapshot structures
1837         for (int f = 1; f <= qState.boap.getFrames(); f++) {
1838           qState.store(f);
1839         }
1840         BOA_.log("Run segmentation from mask file");
1841       }
1842 
1843       /*
1844        * Clean all bOA state.
1845        */
1846       if (b == menuSegmentationReset) {
1847         qState.reset(WindowManager.getCurrentImage(), pluginFactory, viewUpdater);
1848         qState.nest.cleanNest();
1849         imageGroup.clearPaths(1);
1850         setDefualts();
1851         if (qState.boap.frame != imageGroup.getOrgIpl().getSlice()) {
1852           imageGroup.updateToFrame(qState.boap.frame); // move to frame
1853         } else {
1854           updateSliceSelector(); // repaint window explicitly
1855         }
1856       }
1857 
1858       updateWindowState(); // window logic on any change and selectors
1859 
1860       // run segmentation for selected cases
1861       if (run) { // in thread
1862         runBoaThread(qState.boap.frame, qState.boap.frame, false);
1863       }
1864     }
1865 
1866     /**
1867      * If Freeze cell button is clicked.
1868      * 
1869      * @param status on or off this function
1870      */
1871     private void setFreeze(boolean status) {
1872       if (status) {
1873         bnFreezeCell.setLabel("*CANCEL*");
1874         qState.boap.doFreeze = true;
1875         lastTool = IJ.getToolName();
1876         IJ.setTool(Toolbar.LINE);
1877       } else {
1878         bnFreezeCell.setLabel("Freeze");
1879         qState.boap.doFreeze = false;
1880         IJ.setTool(lastTool);
1881       }
1882     }
1883 
1884     /**
1885      * Run segmentation in separate thread.
1886      * 
1887      * @param startFrame start frame
1888      * @param endFrame end frame
1889      * 
1890      * @param interruptible if true cancel button is active.
1891      */
1892     private void runBoaThread(int startFrame, int endFrame, boolean interruptible) {
1893       // run on current frame
1894       sww = new SwingWorker<Boolean, Object>() {
1895         @Override
1896         protected Boolean doInBackground() throws Exception {
1897           setBusyStatus(true, interruptible);
1898           IJ.showStatus("SEGMENTING...");
1899           runBoa(startFrame, endFrame);
1900           return true;
1901         }
1902 
1903         @Override
1904         protected void done() {
1905           try {
1906             get();
1907           } catch (ExecutionException e) {
1908             Throwable cause = e.getCause();
1909             if (cause instanceof BoaException) {
1910               ((BoaException) cause).setMessageSinkType(MessageSinkTypes.NONE);
1911               int framesCompleted = ((BoaException) cause).getFrame();
1912               String ret = ((BoaException) cause).handleException(IJ.getInstance(),
1913                       "FAIL AT " + framesCompleted);
1914               IJ.showStatus("FAIL AT " + framesCompleted);
1915               BOA_.log(ret);
1916             }
1917           } catch (InterruptedException e) {
1918             // TODO Auto-generated catch block
1919             e.printStackTrace();
1920           } finally {
1921             setBusyStatus(false, true);
1922             IJ.showStatus("COMPLETE");
1923           }
1924         }
1925 
1926       };
1927       sww.execute();
1928     }
1929 
1930     /**
1931      * Copy plugin tree from current frame (current state of qState.snakePluginList) to other
1932      * frames.
1933      * 
1934      * @param frames List of frames to copy plugin stack to. Numbered from 1.
1935      * 
1936      * @see #actionPerformed(ActionEvent)
1937      */
1938     private void populatePlugins(List<Integer> frames) {
1939       if (frames.isEmpty()) {
1940         return;
1941       }
1942       SnakePluginList tmp = qState.snakePluginList.getDeepCopy();
1943       int cf = qState.boap.frame;
1944 
1945       sww = new SwingWorker<Boolean, Object>() {
1946         @Override
1947         protected Boolean doInBackground() throws Exception {
1948           setBusyStatus(true, true);
1949           IJ.showStatus("SEGMENTING...");
1950           IJ.showProgress(0, frames.get(frames.size() - 1) - frames.get(0));
1951           // iterate over frames and applies plugins
1952           for (int f : frames) {
1953             // make a deep copy
1954             qState.snakePluginListSnapshots.set(f - 1, tmp.getDeepCopy());
1955             qState.snakePluginListSnapshots.set(f - 1, tmp.getDeepCopy());
1956             // instance separate copy of jar for this plugin (in fact PluginFactory will return here
1957             // reference if this jar is already opened)
1958             qState.snakePluginListSnapshots.get(f - 1).afterSerialize();
1959 
1960             qState.boap.frame = f; // assign to global frame variable
1961             imageGroup.updateToFrame(qState.boap.frame);
1962             recalculatePlugins();
1963             if (isSegBreakHit == true) { // if flag set, stop
1964               isSegBreakHit = false;
1965               break;
1966             }
1967             IJ.showProgress(f, frames.get(frames.size() - 1));
1968           }
1969           qState.boap.frame = cf;
1970           imageGroup.updateToFrame(qState.boap.frame);
1971           return true;
1972         }
1973 
1974         @Override
1975         protected void done() {
1976           setBusyStatus(false, true);
1977           IJ.showStatus("COMPLETE");
1978           IJ.showProgress(2.0); // >1 to erase progress bar
1979         }
1980 
1981       };
1982       sww.execute();
1983 
1984     }
1985 
1986     /**
1987      * Loader of QCONF file in BOA. Initialise all BOA structures and updates window.
1988      * 
1989      * <p>Assign also format converter. This method partially updates UI but some other related
1990      * methods are called from {@link #actionPerformed(ActionEvent)}.
1991      * 
1992      * @param configPath path to QCONF file
1993      * @throws IOException on file problem
1994      * @throws Exception various other problems like e.g json syntax
1995      * @see #updateWindowState()
1996      */
1997     private void loadQconfConfiguration(Path configPath) throws IOException, Exception {
1998       Path filename = configPath.getFileName();
1999       if (filename == null) {
2000         throw new IllegalAccessException("Input path is not file");
2001       }
2002       Serializer<DataContainer> loaded; // loaded instance
2003       // create serializer
2004       Serializer<DataContainer> s = new Serializer<>(DataContainer.class, QuimP.TOOL_VERSION);
2005       s.registerConverter(new Converter170202<>(QuimP.TOOL_VERSION));
2006       s.registerInstanceCreator(DataContainer.class,
2007               new DataContainerInstanceCreator(pluginFactory, viewUpdater));
2008       loaded = s.load(configPath.toString());
2009       // check against image names
2010       if (!loaded.obj.BOAState.boap.getOrgFile().getName()
2011               .equals(qState.boap.getOrgFile().getName())) {
2012         LOGGER.warn("The image opened currently in BOA is different from those"
2013                 + " pointed in configuration file");
2014         log("Trying to apply configuration saved for other image");
2015         YesNoCancelDialog yncd = new YesNoCancelDialog(IJ.getInstance(), "Warning",
2016                 "Trying to load configuration that does not\nmath to"
2017                         + " opened image.\nAre you sure?");
2018         if (!yncd.yesPressed()) {
2019           return;
2020         }
2021       }
2022       // replace orgFile with that already opened. It is possible as BOA can not
2023       // exist without image loaded so this field will always be true.
2024       loaded.obj.BOAState.boap.setOrgFile(qState.boap.getOrgFile());
2025       // replace outputFileCore with current one
2026       String parent;
2027       if (configPath.getParent() != null) {
2028         parent = configPath.getParent().toString();
2029       } else {
2030         parent = "";
2031       }
2032       loaded.obj.BOAState.boap.setOutputFileCore(parent + File.separator + filename.toString());
2033       // closes windows, etc
2034       qState.reset(WindowManager.getCurrentImage(), pluginFactory, viewUpdater);
2035       qState = loaded.obj.BOAState;
2036       imageGroup.updateNest(qState.nest); // reconnect nest to external class
2037       qState.restore(qState.boap.frame); // copy from snapshots to current object
2038       updateSpinnerValues(); // update segmentation gui
2039       // refill frame zoom choice to make possible selection last zoomed cell (called from
2040       // updateChoice)
2041       fillZoomChoice();
2042       // do not recalculatePlugins here because pluginList is empty and this
2043       // method will update finalSnake overriding it by segSnake (because on
2044       // empty list they are just copied)
2045       // updateToFrame calls updateSliceSelector only if there is action of
2046       // changing frame. If loaded frame is the same as current one this event is
2047       // not called.
2048       updateMenus();
2049       if (qState.boap.frame != imageGroup.getOrgIpl().getSlice()) {
2050         // move to frame (will call updateSliceSelector)
2051         imageGroup.updateToFrame(qState.boap.frame);
2052       } else {
2053         updateSliceSelector(); // repaint window explicitly
2054       }
2055       BOA_.log("Configuration read successfully");
2056     }
2057 
2058     /**
2059      * Detect changes in checkboxes and run segmentation for current frame if necessary.
2060      * 
2061      * <p>Transfer parameters from changed GUI element to
2062      * {@link com.github.celldynamics.quimp.BOAState.BOAp} class
2063      * 
2064      * @param e Type of event
2065      * @see com.github.celldynamics.quimp.BOA_.CustomStackWindow#updateWindowState()
2066      */
2067     @Override
2068     public void itemStateChanged(final ItemEvent e) {
2069       LOGGER.trace("EVENT:itemStateChanged");
2070       setFreeze(false); // disable if active
2071       if (qState.boap.doDelete) {
2072         BOA_.log("**WARNING:DELETE IS ON**");
2073       }
2074       boolean run = false; // set to true if any of items changes require to re-run segmentation
2075       Object source = e.getItemSelectable();
2076       if (source == cbPath) {
2077         qState.segParam.showPaths = cbPath.getState();
2078         if (qState.segParam.showPaths) {
2079           this.setImage(imageGroup.getPathsIpl());
2080         } else {
2081           this.setImage(imageGroup.getOrgIpl());
2082         }
2083         if (qState.boap.zoom && !qState.nest.isVacant()) { // set zoom
2084           imageGroup.zoom(canvas, qState.boap.frame, qState.boap.snakeToZoom);
2085         }
2086       } else if (source == cbPrevSnake) {
2087         qState.segParam.use_previous_snake = cbPrevSnake.getState();
2088       } else if (source == cbExpSnake) {
2089         qState.segParam.expandSnake = cbExpSnake.getState();
2090         run = true;
2091       } else if (source == cbContractingDirection) {
2092         qState.segParam.contractingDirection = cbContractingDirection.getState();
2093         qState.segParam.reverseForces();
2094         updateSpinnerValues();
2095         run = true;
2096       } else if (source == cbFirstPluginActiv) { // run in thread
2097         qState.snakePluginList.setActive(0, cbFirstPluginActiv.getState());
2098         setBusyStatus(true, false);
2099         recalculatePlugins();
2100       } else if (source == cbSecondPluginActiv) { // run in thread
2101         qState.snakePluginList.setActive(1, cbSecondPluginActiv.getState());
2102         setBusyStatus(true, false);
2103         recalculatePlugins();
2104       } else if (source == cbThirdPluginActiv) { // run in thread
2105         qState.snakePluginList.setActive(2, cbThirdPluginActiv.getState());
2106         setBusyStatus(true, false);
2107         recalculatePlugins();
2108       }
2109 
2110       // action on menus
2111       if (source == cbMenuPlotOriginalSnakes) { // run in thread
2112         qState.boap.isProcessedSnakePlotted = cbMenuPlotOriginalSnakes.getState();
2113         setBusyStatus(true, false);
2114         recalculatePlugins();
2115       }
2116       if (source == cbMenuPlotHead) {
2117         qState.boap.isHeadPlotted = cbMenuPlotHead.getState();
2118         imageGroup.updateOverlay(qState.boap.frame);
2119       }
2120       if (source == cbMenuZoomFreeze) {
2121         qState.boap.isZoomFreeze = cbMenuZoomFreeze.getState();
2122         if (qState.boap.isZoomFreeze && qState.boap.zoom) { // set in zoom mode
2123           JOptionPane.showMessageDialog(window, QuimpToolsCollection
2124                   .stringWrap("Please un-zoom your view first.", QuimP.LINE_WRAP), "Error",
2125                   JOptionPane.ERROR_MESSAGE);
2126           qState.boap.isZoomFreeze = false;
2127           cbMenuZoomFreeze.setState(false); // unselect
2128         }
2129       }
2130       // actions on Plugin selections
2131       if (source == chFirstPluginName) { // run in thread
2132         LOGGER.debug("Used firstPluginName, val: " + chFirstPluginName.getSelectedItem());
2133         instanceSnakePlugin((String) chFirstPluginName.getSelectedItem(), 0,
2134                 cbFirstPluginActiv.getState());
2135         setBusyStatus(true, false);
2136         recalculatePlugins();
2137       }
2138       if (source == chSecondPluginName) { // run in thread
2139         LOGGER.debug("Used secondPluginName, val: " + chSecondPluginName.getSelectedItem());
2140         instanceSnakePlugin((String) chSecondPluginName.getSelectedItem(), 1,
2141                 cbSecondPluginActiv.getState());
2142         setBusyStatus(true, false);
2143         recalculatePlugins();
2144       }
2145       if (source == chThirdPluginName) { // run in thread
2146         LOGGER.debug("Used thirdPluginName, val: " + chThirdPluginName.getSelectedItem());
2147         instanceSnakePlugin((String) chThirdPluginName.getSelectedItem(), 2,
2148                 cbThirdPluginActiv.getState());
2149         setBusyStatus(true, false);
2150         recalculatePlugins();
2151       }
2152 
2153       // Action on zoom selector
2154       if (source == chZoom) {
2155         LOGGER.trace("zoom val " + chZoom.getSelectedItem());
2156         if (chZoom.getSelectedItem().equals(fullZoom)) { // user selected default position (no zoom)
2157           qState.boap.snakeToZoom = -1; // set negative value to indicate no zoom
2158           qState.boap.zoom = false; // important for other parts (legacy)
2159           imageGroup.unzoom(canvas); // unzoom view
2160           // unfreeze all
2161           if (qState.boap.isZoomFreeze == true) {
2162             for (SnakeHandler sh : qState.nest.getHandlers()) {
2163               sh.unfreezeHandler();
2164             }
2165           }
2166         } else { // zoom here
2167           if (!qState.nest.isVacant()) { // any snakes present
2168             qState.boap.snakeToZoom = Integer.parseInt(chZoom.getSelectedItem()); // get int
2169             qState.boap.zoom = true; // legacy compatibility
2170             // snakeID, not index
2171             SnakeHandler snakeH = qState.nest.getHandlerofId(qState.boap.snakeToZoom);
2172             if (qState.boap.isZoomFreeze == true && snakeH != null
2173                     && snakeH.isStoredAt(qState.boap.frame)) {
2174               for (SnakeHandler sh : qState.nest.getHandlers()) {
2175                 sh.freezeHandler();
2176               } // freeze all except:
2177               snakeH.unfreezeHandler();
2178             }
2179             imageGroup.zoom(canvas, qState.boap.frame, qState.boap.snakeToZoom);
2180           }
2181         }
2182         imageGroup.updateOverlay(qState.boap.frame); // to have proper colors for frozen snakes
2183       }
2184 
2185       updateWindowState(); // window logic on any change
2186       updateChoices(); // only for updating colors after updating window state
2187 
2188       try {
2189         if (run) {
2190           if (supressStateChangeBOArun) { // when spinners are changed
2191             // programmatically they raise the event. this is to block segmentation re-run
2192             LOGGER.debug("supressState");
2193             return;
2194           }
2195           // run on current frame
2196           runBoa(qState.boap.frame, qState.boap.frame);
2197         }
2198       } catch (BoaException be) {
2199         be.setMessageSinkType(MessageSinkTypes.NONE);
2200         BOA_.log(be.handleException(IJ.getInstance(), "Segmentation failed at " + be.getFrame()));
2201       } finally {
2202         setBusyStatus(false, true);
2203       }
2204     }
2205 
2206     /**
2207      * Detect changes in spinners and run segmentation for current frame if necessary.
2208      * 
2209      * <p>Transfer parameters from changed GUI element to
2210      * {@link com.github.celldynamics.quimp.BOAState.BOAp} class
2211      * 
2212      * @param ce Type of event
2213      * @see com.github.celldynamics.quimp.BOA_.CustomStackWindow#updateWindowState()
2214      */
2215     @Override
2216     public void stateChanged(final ChangeEvent ce) {
2217       LOGGER.trace("EVENT:stateChanged");
2218       setFreeze(false); // disable if active
2219       if (qState.boap.doDelete) {
2220         BOA_.log("**WARNING:DELETE IS ON**");
2221       }
2222       boolean run = false; // set to true if any of items changes require to re-run segmentation
2223       Object source = ce.getSource();
2224 
2225       if (source == dsNodeRes) {
2226         JSpinner spinner = (JSpinner) source;
2227         qState.segParam.setNodeRes((Double) spinner.getValue());
2228         run = true;
2229       } else if (source == dsVelCrit) {
2230         JSpinner spinner = (JSpinner) source;
2231         qState.segParam.vel_crit = (Double) spinner.getValue();
2232         run = true;
2233       } else if (source == dsFImage) {
2234         JSpinner spinner = (JSpinner) source;
2235         qState.segParam.f_image = (Double) spinner.getValue();
2236         run = true;
2237       } else if (source == dsFCentral) {
2238         JSpinner spinner = (JSpinner) source;
2239         qState.segParam.f_central = (Double) spinner.getValue();
2240         run = true;
2241       } else if (source == dsFContract) {
2242         JSpinner spinner = (JSpinner) source;
2243         qState.segParam.f_contract = (Double) spinner.getValue();
2244         run = true;
2245       } else if (source == dsFinalShrink) {
2246         JSpinner spinner = (JSpinner) source;
2247         qState.segParam.finalShrink = (Double) spinner.getValue();
2248         run = true;
2249       } else if (source == isMaxIterations) {
2250         JSpinner spinner = (JSpinner) source;
2251         qState.segParam.max_iterations = (Integer) spinner.getValue();
2252         run = true;
2253       } else if (source == isBlowup) {
2254         JSpinner spinner = (JSpinner) source;
2255         qState.segParam.blowup = (Integer) spinner.getValue();
2256         run = true;
2257       } else if (source == isSampletan) {
2258         JSpinner spinner = (JSpinner) source;
2259         qState.segParam.sample_tan = (Integer) spinner.getValue();
2260         run = true;
2261       } else if (source == isSamplenorm) {
2262         JSpinner spinner = (JSpinner) source;
2263         qState.segParam.sample_norm = (Integer) spinner.getValue();
2264         run = true;
2265       }
2266 
2267       updateWindowState(); // window logic on any change
2268 
2269       try {
2270         if (run) {
2271           // when spinners are changed programmatically they raise the event. this is to block
2272           // segmentation re-run
2273           if (supressStateChangeBOArun) {
2274             LOGGER.debug("supressState");
2275             return;
2276           }
2277           // run on current frame
2278           runBoa(qState.boap.frame, qState.boap.frame);
2279         }
2280       } catch (BoaException be) {
2281         be.setMessageSinkType(MessageSinkTypes.NONE);
2282         BOA_.log(be.handleException(IJ.getInstance(), "Segmentation failed " + be.getFrame()));
2283       }
2284     }
2285 
2286     /**
2287      * Update the frame label, overlay, frame and set zoom Called when user clicks on slice
2288      * selector in IJ window.
2289      */
2290     @Override
2291     public void updateSliceSelector() {
2292       super.updateSliceSelector();
2293       LOGGER.trace("EVENT:updateSliceSelector");
2294       if (!qState.boap.singleImage) {
2295         zSelector.setValue(imp.getCurrentSlice()); // this is delayed in
2296         // super.updateSliceSelector force it now
2297       }
2298       if (qState.boap.editMode) {
2299         // BOA_.log("next frame in edit mode");
2300         stopEdit();
2301       }
2302 
2303       if (!qState.boap.singleImage) {
2304         qState.boap.frame = imp.getCurrentSlice();
2305         frameLabel.setText("" + qState.boap.frame);
2306       }
2307       imageGroup.updateOverlay(qState.boap.frame); // draw overlay
2308       imageGroup.setIpSliceAll(qState.boap.frame);
2309 
2310       // zoom to snake zero
2311       if (qState.boap.zoom && !qState.nest.isVacant()) {
2312         imageGroup.zoom(canvas, qState.boap.frame, qState.boap.snakeToZoom);
2313       }
2314       // if in edit, save current edit and start edit of next frame if exists
2315       boolean wasInEdit = qState.boap.editMode;
2316       if (wasInEdit) {
2317         bnEdit.setLabel("*STOP EDIT*");
2318         BOA_.log("**EDIT IS ON**");
2319         qState.boap.editMode = true;
2320         lastTool = IJ.getToolName();
2321         IJ.setTool(Toolbar.LINE);
2322         editSeg(0, 0, qState.boap.frame);
2323         IJ.setTool(lastTool);
2324       }
2325       LOGGER.trace("Snakes at this frame: " + qState.nest.getSnakesforFrame(qState.boap.frame));
2326       if (!isSegRunning) {
2327         // do not update or restore state when we hit this event from runBoa() method (through
2328         // setIpSliceAll(int))
2329         qState.restore(qState.boap.frame);
2330         updateSpinnerValues();
2331         updateWindowState();
2332       }
2333     }
2334 
2335     /**
2336      * Turn delete mode off by setting proper value in
2337      * {@link com.github.celldynamics.quimp.BOAState.BOAp}.
2338      */
2339     void switchOffDelete() {
2340       qState.boap.doDelete = false;
2341       bnDel.setLabel("Delete cell");
2342     }
2343 
2344     /*
2345      * (non-Javadoc)
2346      * 
2347      * @see ij.gui.ImageWindow#setImage(ij.ImagePlus)
2348      */
2349     @Override
2350     public void setImage(ImagePlus imp2) {
2351       double m = this.ic.getMagnification();
2352       Dimension dem = this.ic.getSize();
2353       super.setImage(imp2);
2354       this.ic.setMagnification(m);
2355       this.ic.setSize(dem);
2356     }
2357 
2358     /**
2359      * Turn truncate mode off by setting proper value in
2360      * {@link com.github.celldynamics.quimp.BOAState.BOAp}.
2361      */
2362     void switchOfftruncate() {
2363       qState.boap.doDeleteSeg = false;
2364       bnDelSeg.setLabel("Truncate Seg");
2365     }
2366 
2367     /**
2368      * Set frame interval and scale on BOA window..
2369      */
2370     void setScalesText() {
2371       pixelLabel.setText("Scale: " + IJ.d2s(qState.boap.getImageScale(), 6) + " \u00B5m");
2372       fpsLabel.setText("F Interval: " + IJ.d2s(qState.boap.getImageFrameInterval(), 3) + " s");
2373     }
2374 
2375   } // end of CustomStackWindow
2376 
2377   /**
2378    * Creates instance (through SnakePluginList) of plugin of given name on given UI slot.
2379    * 
2380    * <p>Decides if plugin will be created or destroyed basing on plugin name from Choice list
2381    * 
2382    * @param selectedPlugin Name of plugin returned from UI elements
2383    * @param slot Slot of plugin
2384    * @param act Indicates if plugins is activated in GUI
2385    * @see com.github.celldynamics.quimp.SnakePluginList
2386    */
2387   private void instanceSnakePlugin(final String selectedPlugin, int slot, boolean act) {
2388 
2389     try {
2390       // get instance using plugin name (obtained from getPluginNames from PluginFactory
2391       if (selectedPlugin != NONE) { // do no pass NONE to pluginFact
2392         qState.snakePluginList.setInstance(slot, selectedPlugin, act); // build instance
2393       } else {
2394         if (qState.snakePluginList.getInstance(slot) != null) {
2395           qState.snakePluginList.getInstance(slot).showUi(false);
2396         }
2397         qState.snakePluginList.deletePlugin(slot);
2398       }
2399     } catch (QuimpPluginException e) {
2400       IJ.error("Plugin " + selectedPlugin + " cannot be loaded.", "Reason: " + e.getMessage());
2401       LOGGER.debug(e.getMessage(), e);
2402     }
2403   }
2404 
2405   /**
2406    * Set busy status for BOA window.
2407    * 
2408    * <p>Window is inactive for busy status. Setting flag <tt>interruptible</tt> enables Cancel
2409    * button that breaks current operation.
2410    * 
2411    * @param busy True if busy, false otherwise
2412    * @param interruptible true for enabling Cancel button. Ignored if busy==false
2413    */
2414   private void setBusyStatus(boolean busy, boolean interruptible) {
2415     if (busy == true) {
2416       window.bnSeg.setBackground(Color.RED);
2417       window.bnSeg.setLabel("Busy");
2418       if (interruptible) {
2419         window.enableUiInterruptile(false);
2420       } else {
2421         window.enableUi(false);
2422       }
2423     } else {
2424       window.bnSeg.setBackground(null);
2425       window.bnSeg.setLabel("SEGMENT");
2426       window.enableUi(true);
2427     }
2428 
2429   }
2430 
2431   /**
2432    * Start segmentation process on range of frames.
2433    * 
2434    * <p>This method is called for update only current view as well (<tt>startF</tt> ==
2435    * <tt>endF</tt>). It also go through plugin stack.
2436    * 
2437    * @param startF start frame
2438    * @param endF end frame
2439    * @throws BoaException on any error
2440    */
2441   public void runBoa(int startF, int endF) throws BoaException {
2442     LOGGER.debug("run BOA");
2443     isSegBreakHit = false;
2444     isSegRunning = true;
2445     if (qState.nest.isVacant() || qState.nest.allFrozen()) {
2446       BOA_.log("Nothing to segment!");
2447       isSegRunning = false;
2448       return;
2449     }
2450     try {
2451       IJ.showProgress(0, endF - startF);
2452 
2453       qState.nest.resetForFrame(startF);
2454       if (!qState.segParam.expandSnake) {
2455         // blowup snake ready for contraction (only those not starting at or after the startF)
2456         constrictor.loosen(qState.nest, startF);
2457       } else {
2458         constrictor.implode(qState.nest, startF);
2459       }
2460       SnakeHandler snH;
2461       int s = 0;
2462       Snake snake;
2463       imageGroup.clearPaths(startF);
2464       LOGGER.debug("Use options: " + qState.segParam.toString());
2465       for (qState.boap.frame = startF; qState.boap.frame <= endF; qState.boap.frame++) {
2466         if (isSegBreakHit == true) {
2467           qState.boap.frame--;
2468           isSegBreakHit = false;
2469           break;
2470         }
2471         // per frame
2472         imageGroup.setProcessor(qState.boap.frame);
2473         imageGroup.setIpSliceAll(qState.boap.frame);
2474 
2475         try {
2476           if (qState.boap.frame != startF) { // expand snakes for next frame
2477             if (!qState.segParam.use_previous_snake) {
2478               qState.nest.resetForFrame(qState.boap.frame); // #274 block here as well? liveSnake
2479             } else {
2480               if (!qState.segParam.expandSnake) {
2481                 constrictor.loosen(qState.nest, qState.boap.frame); // #274 here? this is about LS
2482               } else {
2483                 constrictor.implode(qState.nest, qState.boap.frame); // #274 and here
2484               }
2485             }
2486           }
2487 
2488           for (s = 0; s < qState.nest.size(); s++) { // for each snake
2489             snH = qState.nest.getHandler(s);
2490             snake = snH.getLiveSnake();
2491             try {
2492               if (!snake.alive || qState.boap.frame < snH.getStartFrame()) {
2493                 continue;
2494               }
2495               // process all snakes even if frozen to control overlaps (computed for liveSnakes) but
2496               // do not store any live snake from frozen snake
2497               if (!snH.isSnakeHandlerFrozen()) {
2498                 imageGroup.drawPath(snake, qState.boap.frame); // pre tightned snake on path
2499                 tightenSnake(snake);
2500                 imageGroup.drawPath(snake, qState.boap.frame); // post tightned snake on path
2501                 snH.backupLiveSnake(qState.boap.frame);
2502                 Snake out = iterateOverSnakePlugins(snake);
2503                 snH.storeThisSnake(out, qState.boap.frame); // store resulting snake as final
2504               } else {
2505                 // overlaps are tested for liveSnakes (loosen) so update liveSnake to result of
2506                 // segmentation for frozen snakehandler
2507                 snH.copyFromFinalToLive(qState.boap.frame);
2508                 LOGGER.debug("SnakeHandler " + snH.getID() + " is frozen");
2509               }
2510             } catch (QuimpPluginException qpe) {
2511               // must be rewritten with whole runBOA #65 #67
2512               qpe.setMessageSinkType(MessageSinkTypes.NONE);
2513               BOA_.log(qpe.handleException(null, "Error in filter module"));
2514               snH.storeLiveSnake(qState.boap.frame); // store segmented nonmodified
2515             } catch (BoaException be) { // from tighten
2516               imageGroup.drawPath(snake, qState.boap.frame); // failed position
2517               snH.storeLiveSnake(qState.boap.frame);
2518               snH.backupLiveSnake(qState.boap.frame);
2519               qState.nest.kill(snH);
2520               snake.unfreezeAll();
2521               be.setMessageSinkType(MessageSinkTypes.NONE);
2522               BOA_.log(be.handleException(null,
2523                       "Snake " + snake.getSnakeID() + " died, frame " + qState.boap.frame));
2524               isSegRunning = false;
2525               if (qState.nest.allDead()) { // end of processing (see condition in catch)
2526                 throw new BoaException("All snakes dead: " + be.getMessage(), qState.boap.frame, 1);
2527               }
2528             }
2529           }
2530           // imageGroup.updateOverlay(qState.boap.frame); // redraw display
2531           IJ.showProgress(qState.boap.frame, endF);
2532         } catch (BoaException be) {
2533           isSegRunning = false;
2534           if (!qState.segParam.use_previous_snake) {
2535             imageGroup.setIpSliceAll(qState.boap.frame);
2536             imageGroup.updateOverlay(qState.boap.frame);
2537           } else { // end iterating if all snakes dead and we use previous
2538             throw be; // do no add LOGGER here #278
2539           }
2540         } finally {
2541           // historyLogger.addEntry("Processing", qState);
2542           qState.store(qState.boap.frame); // always remember state of the BOA that is
2543           // used for segmentation
2544         }
2545       }
2546       qState.boap.frame = endF;
2547     } catch (BoaException be) { // these from unexpected stopping of alg
2548       if (be.getFrame() == 0) { // if not set by thrower
2549         be.setFrame(qState.boap.frame);
2550       }
2551       throw be; // just rethrow them
2552     } catch (Exception e) { // any other (should not happen)
2553       // do no add LOGGER here #278
2554       BoaExceptionBoaException.html#BoaException">BoaException be = new BoaException(e);
2555       be.setFrame(qState.boap.frame);
2556       throw be;
2557     } finally {
2558       isSegRunning = false;
2559       imageGroup.updateOverlay(qState.boap.frame); // update on error
2560       IJ.showProgress(2.0); // >1 to erase progress bar
2561     }
2562 
2563   }
2564 
2565   /**
2566    * Perform AC segmentation. Tighten snake around object.
2567    * 
2568    * <p>This method starts with snakes ({@link SnakeHandler#getLiveSnake()} that were blown up by
2569    * {@link Constrictor#loosen(Nest, int)} counteracting overlaps. If
2570    * {@link BOAState.SegParam#use_previous_snake} was not set, initial snake is produced from
2571    * original ROI by {@link Nest#resetForFrame(int)}. Then
2572    * {@link Constrictor#constrict(Snake, ImageProcessor)} is called many times, each time liveSnake
2573    * is moved slightly. Note that <b>liveSnake</b> is the same for each frame for given
2574    * {@link SnakeHandler}, this is why it can be used for seeding next frame.
2575    * 
2576    * @param snake snake to process
2577    * @throws BoaException if there is too less nodes left
2578    * @see SegParam#max_iterations
2579    */
2580   private void tightenSnake(final Snake snake) throws BoaException {
2581 
2582     int i;
2583 
2584     for (i = 0; i < qState.segParam.max_iterations; i++) { // iter constrict snake
2585       // if snakes are expanded from cell inside, testing against overlapping shuld be done in
2586       // expanding step but not in prparatory (loosen) as for constricting
2587       // if (qState.segParam.contractingDirection == false) { // expand from inside
2588       // constrictor.freezeProxSnakes(qState.nest, qState.boap.frame);
2589       // }
2590       if (i % qState.boap.cut_every == 0) {
2591         snake.cutLoops(); // cut out loops every p.cut_every timesteps
2592       }
2593       if (i % 10 == 0 && i != 0) {
2594         snake.correctDistance(true);
2595       }
2596       if (constrictor.constrict(snake, imageGroup.getOrgIp())) { // if all nodes frozen
2597         break;
2598       }
2599       if (i % 4 == 0) {
2600         imageGroup.drawPath(snake, qState.boap.frame); // draw current snake
2601       }
2602 
2603       if ((snake.getNumPoints() / snake.startingNnodes) > qState.boap.NMAX) {
2604         // if max nodes reached (as % starting) prompt for reset
2605         if (qState.segParam.use_previous_snake) {
2606           // imageGroup.drawContour(snake, frame);
2607           // imageGroup.updateAndDraw();
2608           throw new BoaException(
2609                   "Frame " + qState.boap.frame + "-max nodes reached " + snake.getNumPoints(),
2610                   qState.boap.frame, 1);
2611         } else {
2612           BOA_.log("Frame " + qState.boap.frame + "-max nodes reached..continue");
2613           break;
2614         }
2615       }
2616     }
2617     snake.unfreezeAll(); // set freeze tag back to false
2618 
2619     if (!qState.segParam.expandSnake) { // shrink a bit to get final outline
2620       if (BOA_.qState.segParam.contractingDirection) { // standard behaviour
2621         snake.scaleSnake(-BOA_.qState.segParam.finalShrink, 0.5, false);
2622       } else { // expanding from cell inside, set the same as contracting
2623         snake.scaleSnake(-BOA_.qState.segParam.finalShrink, 0.5, false);
2624       }
2625     }
2626     snake.cutLoops();
2627     snake.cutIntersects();
2628   }
2629 
2630   /**
2631    * Process Snake by all active plugins.
2632    * 
2633    * <p>Processed Snake is returned as new Snake with the same ID. Input snake is not modified. For
2634    * empty plugin list it just return input snake
2635    *
2636    * <p>This method supports two interfaces:
2637    * {@link com.github.celldynamics.quimp.plugin.snakes.IQuimpBOAPoint2dFilter},
2638    * {@link com.github.celldynamics.quimp.plugin.snakes.IQuimpBOASnakeFilter}
2639    * 
2640    * <p>It uses smart method to detect which interface is used for every slot to avoid unnecessary
2641    * conversion between data. <tt>previousConversion</tt> keeps what interface was used on
2642    * previous slot in plugin stack. Then for every plugin data are converted if current plugin
2643    * differs from previous one. Converted data are kept in <tt>snakeToProcess</tt> and
2644    * <tt>dataToProcess</tt> but only one of these variables is valid in given time. Finally after
2645    * last plugin data are converted to Snake.
2646    * 
2647    * @param snake snake to be processed
2648    * @return Processed snake or original input one when there is no plugin selected
2649    * @throws QuimpPluginException on plugin error
2650    * @throws BoaException on defective snake
2651    */
2652   private Snakeke">Snake iterateOverSnakePlugins(final Snake snake)
2653           throws QuimpPluginException, BoaException {
2654     final int ipoint = 0; // define IQuimpPoint2dFilter interface
2655     final int isnake = 1; // define IQuimpPoint2dFilter interface
2656     // type of previous plugin. Define if data should be converted for current plugin
2657     int previousConversion = isnake; // IQuimpSnakeFilter is default interface
2658     Snake outsnake = snake; // if there is no plugin just return input snake
2659     Snake snakeToProcess = snake; // data to be processed, input snake on beginning
2660     // data to process in format of list
2661     // null but it will be overwritten in loop because first "if" fires always (previousConversion
2662     // is set to isnake) on beginning, if first plugin is ipoint type
2663     List<Point2d> dataToProcess = null;
2664     if (!qState.snakePluginList.isRefListEmpty()) {
2665       LOGGER.debug("sPluginList not empty");
2666       for (Plugin qsP : qState.snakePluginList.getList()) { // iterate over list
2667         if (!qsP.isExecutable()) {
2668           continue; // no plugin on this slot or not active
2669         }
2670         if (qsP.getRef() instanceof IQuimpPluginAttachImage) {
2671           ((IQuimpPluginAttachImage) qsP.getRef()).attachImage(imageGroup.getOrgIp());
2672         }
2673         if (qsP.getRef() instanceof IQuimpBOAPoint2dFilter) { // check interface type
2674           if (previousConversion == isnake) { // previous was IQuimpSnakeFilter
2675             dataToProcess = snakeToProcess.asList(); // and data needs to be converted
2676           }
2677           IQuimpBOAPoint2dFilterithub/celldynamics/quimp/plugin/snakes/IQuimpBOAPoint2dFilter.html#IQuimpBOAPoint2dFilter">IQuimpBOAPoint2dFilter qsPcast = (IQuimpBOAPoint2dFilter) qsP.getRef();
2678           qsPcast.attachData(dataToProcess);
2679           dataToProcess = qsPcast.runPlugin(); // store result in input variable
2680           previousConversion = ipoint;
2681         }
2682         if (qsP.getRef() instanceof IQuimpBOASnakeFilter) { // check interface type
2683           if (previousConversion == ipoint) { // previous was IQuimpPoint2dFilter
2684             // and data must be converted to snake from dataToProcess
2685             snakeToProcess = new QuimpDataConverter(dataToProcess).getSnake(snake.getSnakeID());
2686           }
2687           IQuimpBOASnakeFilter/github/celldynamics/quimp/plugin/snakes/IQuimpBOASnakeFilter.html#IQuimpBOASnakeFilter">IQuimpBOASnakeFilter qsPcast = (IQuimpBOASnakeFilter) qsP.getRef();
2688           qsPcast.attachData(snakeToProcess);
2689           snakeToProcess = qsPcast.runPlugin(); // store result as snake for next plugin
2690           previousConversion = isnake;
2691         }
2692       }
2693       // after loop previousConversion points what plugin was last and actual data
2694       // must be converted to snake
2695       switch (previousConversion) {
2696         case ipoint: // last plugin was IQuimpPoint2dFilter - convert to Snake
2697           outsnake = new QuimpDataConverter(dataToProcess).getSnake(snake.getSnakeID());
2698           break;
2699         case isnake: // last plugin was IQuimpSnakeFilter - do not convert
2700           outsnake = snakeToProcess;
2701           outsnake.setSnakeID(snake.getSnakeID()); // copy old id in case if user forgot
2702           break;
2703         default:
2704           throw new IllegalArgumentException("Unknown previousConversion");
2705       }
2706     } else {
2707       LOGGER.debug("sPluginList empty");
2708     }
2709     return outsnake;
2710   }
2711 
2712   /**
2713    * Sets the scales.
2714    * 
2715    * <p>Scale and interval fields are already initialised in {@link BOAState} constructor from
2716    * loaded image. If image does not have proper scale or interval, defaults from
2717    * {@link BOAState.BOAp#setImageScale(double)} and
2718    * {@link BOAState.BOAp#setImageFrameInterval(double)} are taken.
2719    * 
2720    * <p>All stats are evaluated using scales stored in tiff file so those values put here by user
2721    * are copied to image by {@link #updateImageScale()}.
2722    */
2723   void setScales() {
2724     GenericDialog gd = new GenericDialog("Set image scale", window);
2725     gd.addNumericField("Frame interval (seconds)", qState.boap.getImageFrameInterval(), 3);
2726     gd.addNumericField("Pixel width (\u00B5m)", qState.boap.getImageScale(), 6);
2727     gd.showDialog();
2728 
2729     double tempFI = gd.getNextNumber(); // force to check for errors
2730     double tempP = gd.getNextNumber();
2731 
2732     if (gd.invalidNumber()) {
2733       IJ.error("Values invalid");
2734       BOA_.log("Scale was not updated:\n\tinvalid input");
2735     } else if (gd.wasOKed()) {
2736       qState.boap.setImageFrameInterval(tempFI);
2737       qState.boap.setImageScale(tempP);
2738       updateImageScale();
2739       BOA_.log("Scale successfully updated");
2740     }
2741 
2742   }
2743 
2744   /**
2745    * Update image scale.
2746    */
2747   void updateImageScale() {
2748     imageGroup.getOrgIpl().getCalibration().frameInterval = qState.boap.getImageFrameInterval();
2749     imageGroup.getOrgIpl().getCalibration().pixelHeight = qState.boap.getImageScale();
2750     imageGroup.getOrgIpl().getCalibration().pixelWidth = qState.boap.getImageScale();
2751   }
2752 
2753   /**
2754    * Load snakes from snQP file.
2755    *
2756    * @return true, if successful
2757    */
2758   private boolean loadSnakes() {
2759 
2760     YesNoCancelDialog yncd = new YesNoCancelDialog(IJ.getInstance(), "Load associated snakes?",
2761             "\tLoad associated snakes?\n");
2762     if (!yncd.yesPressed()) {
2763       return false;
2764     }
2765 
2766     OutlineHandlerHandler.html#OutlineHandler">OutlineHandler otlineH = new OutlineHandler(qState.boap.readQp);
2767     if (!otlineH.readSuccess) {
2768       BOA_.log("Could not read in snakes");
2769       return false;
2770     }
2771     // convert to BOA snakes
2772 
2773     qState.nest.addOutlinehandler(otlineH);
2774     imageGroup.setProcessor(otlineH.getStartFrame());
2775     imageGroup.updateOverlay(otlineH.getStartFrame());
2776     BOA_.log("Successfully read snakes");
2777     return true;
2778   }
2779 
2780   /**
2781    * Add ROI to Nest.
2782    * 
2783    * <p>This method is called on selection that should contain object to be segmented. Initialise
2784    * Snake object in Nest and it performs also initial segmentation of selected cell.
2785    * 
2786    * @param r ROI object (IJ)
2787    * @param f number of current frame
2788    * @see #tightenSnake(Snake)
2789    */
2790   // @SuppressWarnings("unchecked")
2791   void addCell(final Roi r, int f) {
2792     SnakeHandler snakeH = qState.nest.addHandler(r, f);
2793     if (snakeH == null) { // cell failed to initialise
2794       return;
2795     }
2796     Snake snake = snakeH.getLiveSnake();
2797     imageGroup.setProcessor(f);
2798     try {
2799       LOGGER.debug("Use options: " + qState.segParam.toString());
2800       imageGroup.drawPath(snake, f); // pre tightned snake on path
2801       tightenSnake(snake);
2802       imageGroup.drawPath(snake, f); // post tightned snake on path
2803       snakeH.backupLiveSnake(f);
2804       Snake out = iterateOverSnakePlugins(snake); // process segmented snake by plugins
2805       snakeH.storeThisSnake(out, f); // store processed snake as final
2806 
2807       // if any problem with plugin or other, store snake without modification
2808       // because snake.asList() returns copy
2809     } catch (QuimpPluginException qpe) {
2810       qpe.setMessageSinkType(MessageSinkTypes.NONE);
2811       BOA_.log(qpe.handleException(null, "Error in filter module"));
2812       snakeH.storeLiveSnake(f);
2813     } catch (BoaException be) {
2814       snakeH.deleteStoreAt(f);
2815       snakeH.kill();
2816       snakeH.backupLiveSnake(f);
2817       snakeH.storeLiveSnake(f);
2818       be.setMessageSinkType(MessageSinkTypes.NONE);
2819       BOA_.log(be.handleException(null, "New snake failed to converge"));
2820     } catch (Exception e) {
2821       IJ.error("Error", e.getMessage());
2822       LOGGER.debug(e.getMessage(), e);
2823     } finally {
2824       imageGroup.updateOverlay(f);
2825       // historyLogger.addEntry("Added cell", qState);
2826       qState.store(f); // always remember state of the BOA after modification of UI
2827     }
2828 
2829   }
2830 
2831   /**
2832    * Delete SnakeHandler using the snake clicked by user.
2833    * 
2834    * <p>Method searches the snake in Nest that is on current frame and its centroid is close enough
2835    * to clicked point. If found, the whole SnakeHandler (all Snakes of the same ID across frames)
2836    * is deleted.
2837    * 
2838    * @param offScreenX clicked coordinate
2839    * @param offScreenY clicked coordinate
2840    * @param frame current frame
2841    * @return true if handler deleted, false if not (because user does not click it)
2842    * @see #freezeCell(int, int, int)
2843    */
2844   boolean deleteCell(int offScreenX, int offScreenY, int frame) {
2845     if (qState.nest.isVacant()) {
2846       return false;
2847     }
2848 
2849     SnakeHandler sh = qState.nest.findClosestTo(offScreenX, offScreenY, frame, 10);
2850     if (sh != null) { // if closest < 10, delete it
2851       BOA_.log("Deleted cell " + sh.getID());
2852       qState.nest.removeHandler(sh);
2853       imageGroup.updateOverlay(frame);
2854       window.switchOffDelete();
2855       return true;
2856     } else {
2857       BOA_.log("Click the cell centre to delete");
2858     }
2859     return false;
2860   }
2861 
2862   /**
2863    * Freeze SnakeHandler closes to specified coordinates.
2864    * 
2865    * <p>Coordinates relate to Snake from SnakeHandler.
2866    * 
2867    * @param offScreenX clicked coordinate
2868    * @param offScreenY clicked coordinate
2869    * @param frame frame
2870    * @return true on success.
2871    * @see #deleteCell(int, int, int)
2872    */
2873   boolean freezeCell(int offScreenX, int offScreenY, int frame) {
2874     if (qState.nest.isVacant()) {
2875       return false;
2876     }
2877 
2878     SnakeHandler sh = qState.nest.findClosestTo(offScreenX, offScreenY, frame, 10);
2879     if (sh != null) { // if closest < 10, delete it
2880       if (sh.isSnakeHandlerFrozen()) {
2881         sh.unfreezeHandler();
2882         BOA_.log("Unfreezed cell " + sh.getID());
2883       } else {
2884         sh.freezeHandler();
2885         BOA_.log("Freezed cell " + sh.getID());
2886       }
2887       imageGroup.updateOverlay(frame);
2888       window.setFreeze(false);
2889       return true;
2890     } else {
2891       BOA_.log("Click the cell centre to freeze");
2892     }
2893     return false;
2894 
2895   }
2896 
2897   /**
2898    * Delete segmentation.
2899    *
2900    * @param x the x
2901    * @param y the y
2902    * @param frame the frame
2903    */
2904   void deleteSegmentation(int x, int y, int frame) {
2905     SnakeHandler snakeH;
2906     Snake snake;
2907     ExtendedVector2d snakeV;
2908     ExtendedVector2dExtendedVector2d.html#ExtendedVector2d">ExtendedVector2d mmV = new ExtendedVector2d(x, y);
2909     List<Double> distance = new ArrayList<Double>();
2910 
2911     for (int i = 0; i < qState.nest.size(); i++) { // calc all distances
2912       snakeH = qState.nest.getHandler(i);
2913 
2914       if (snakeH.isStoredAt(frame)) {
2915         snake = snakeH.getStoredSnake(frame);
2916         snakeV = snake.getCentroid();
2917         distance.add(ExtendedVector2d.lengthP2P(mmV, snakeV));
2918       } else {
2919         distance.add(9999.0);
2920       }
2921     }
2922 
2923     int minIndex = QuimPArrayUtils.minListIndex(distance);
2924     // BOA_.log("Debug: closest index " + minIndex + ", id " +
2925     // nest.getHandler(minIndex).getID());
2926     if (distance.get(minIndex) < 10) { // if closest < 10, delete it
2927       BOA_.log("Deleted snake " + qState.nest.getHandler(minIndex).getID() + " from " + frame
2928               + " onwards");
2929       snakeH = qState.nest.getHandler(minIndex);
2930       snakeH.deleteStoreFrom(frame);
2931       imageGroup.updateOverlay(frame);
2932       window.switchOfftruncate();
2933     } else {
2934       BOA_.log("Click the cell centre to delete");
2935     }
2936   }
2937 
2938   /**
2939    * Called when user click Edit button.
2940    * 
2941    * @param x Coordinate of clicked point
2942    * @param y Coordinate of clicked point
2943    * @param frame current frame in stack
2944    * @see #stopEdit()
2945    * @see com.github.celldynamics.quimp.BOA_.CustomStackWindow#updateSliceSelector()
2946    */
2947   void editSeg(int x, int y, int frame) {
2948     SnakeHandler snakeH;
2949     Snake snake;
2950     ExtendedVector2d snakeV;
2951     ExtendedVector2dExtendedVector2d.html#ExtendedVector2d">ExtendedVector2d mmV = new ExtendedVector2d(x, y);
2952     double[] distance = new double[qState.nest.size()];
2953 
2954     for (int i = 0; i < qState.nest.size(); i++) { // calc all distances
2955       snakeH = qState.nest.getHandler(i);
2956       if (snakeH.isStoredAt(frame)) {
2957         snake = snakeH.getStoredSnake(frame);
2958         snakeV = snake.getCentroid();
2959         distance[i] = ExtendedVector2d.lengthP2P(mmV, snakeV);
2960       }
2961     }
2962     int minIndex = QuimPArrayUtils.minArrayIndex(distance);
2963     if (distance[minIndex] < 10 || qState.nest.size() == 1) { // if closest < 10, edit it
2964       snakeH = qState.nest.getHandler(minIndex);
2965       qState.boap.editingID = minIndex; // sH.getID();
2966       BOA_.log("Editing cell " + snakeH.getID());
2967       imageGroup.clearOverlay();
2968 
2969       Roi r;
2970       if (qState.boap.useSubPixel == true) {
2971         r = snakeH.getStoredSnake(frame).asPolyLine();
2972       } else {
2973         r = snakeH.getStoredSnake(frame).asIntRoi();
2974       }
2975       // Roi r = sH.getStoredSnake(frame).asFloatRoi();
2976       Roi.setColor(Color.cyan);
2977       canvas.getImage().setRoi(r);
2978     } else {
2979       BOA_.log("Click a cell centre to edit");
2980     }
2981   }
2982 
2983   /**
2984    * Called when user ends editing.
2985    * 
2986    * @see com.github.celldynamics.quimp.BOA_.CustomStackWindow#updateSliceSelector()
2987    */
2988   void stopEdit() {
2989     Roi r = canvas.getImage().getRoi();
2990     Roi.setColor(Color.yellow);
2991     SnakeHandler snakeH = qState.nest.getHandler(qState.boap.editingID);
2992     snakeH.storeRoi((PolygonRoi) r, qState.boap.frame); // store as final snake
2993     // copy to segSnakes array
2994     Snake stored = snakeH.getStoredSnake(qState.boap.frame);
2995     snakeH.backupThisSnake(stored, qState.boap.frame);
2996     // and run filters and store in final again
2997     try {
2998       Snake out = iterateOverSnakePlugins(stored); // process segmented snake by plugins
2999       snakeH.storeThisSnake(out, qState.boap.frame); // store processed snake as final
3000     } catch (QuimpException e) {
3001       e.setMessageSinkType(MessageSinkTypes.NONE);
3002       BOA_.log(e.handleException(null, "Error in filter module."));
3003       // TODO now stored in original snake (by storeRoi) but here can be decision what to do
3004     }
3005     canvas.getImage().killRoi();
3006     imageGroup.updateOverlay(qState.boap.frame);
3007     qState.boap.editingID = -1;
3008   }
3009 
3010   /**
3011    * Delete seg.
3012    *
3013    * @param x the x
3014    * @param y the y
3015    */
3016   void deleteSeg(int x, int y) {
3017   }
3018 
3019   /**
3020    * Initialising all data saving and exporting results to disk and IJ.
3021    * 
3022    * @param finish if false file is updated, true created new
3023    */
3024   private void finish(boolean finish) {
3025     IJ.showStatus("BOA-FINISHING");
3026     YesNoCancelDialog ync;
3027     File testF;
3028     boolean saveStats = true; // indicate if stQP file should be saved separatelly.
3029     LOGGER.debug(qState.segParam.toString());
3030     for (SnakeHandler sh : qState.nest.getHandlers()) {
3031       sh.findLastFrame(); // make sure that endFrame points good frame
3032     }
3033     imageGroup.getOrgIpl().deleteRoi(); // clean all roi for qState.nest.analyse
3034     if (qState.boap.saveSnake) {
3035       try {
3036         // check whether there is case saved and warn user
3037         testF = new File(qState.boap.deductNewParamFileName());
3038         // to check only once
3039         boolean testFileExists = testF.exists() && !testF.isDirectory();
3040         LOGGER.trace("Test for QCONF: " + testF.toString());
3041         // this field is set on loading of QCONF thus BOA will ask to save in the same
3042         // folder
3043         // show dialog if we are in create mode OR QCONF does not exist (user clicked update withour
3044         // saving first)
3045         if (finish || !testFileExists) {
3046           String saveIn = BOA_.qState.boap.getOutputFileCore().getParent();
3047           SaveDialog sd = new SaveDialog("Save segmentation data...", saveIn,
3048                   BOA_.qState.boap.getFileName() + FileExtensions.newConfigFileExt, "");
3049           if (sd.getFileName() == null) {
3050             BOA_.log("Save canceled");
3051             return;
3052           }
3053           // This initialize various filenames that can be accessed by other modules (also qconf)
3054           BOA_.qState.boap.setOutputFileCore(sd.getDirectory() + sd.getFileName());
3055           testF = new File(qState.boap.deductNewParamFileName());
3056           testFileExists = testF.exists() && !testF.isDirectory();
3057         }
3058 
3059         if (testFileExists) {
3060           ync = new YesNoCancelDialog(window, "Save Segmentation",
3061                   QuimpToolsCollection.stringWrap("You are about to override previous results ("
3062                           + testF.toString() + "). Is it ok?", QuimP.LINE_WRAP));
3063           if (!ync.yesPressed()) {
3064             return;
3065           }
3066         }
3067         // write operations
3068         // blocked by #263, enabled by 228
3069         if (QuimP.newFileFormat.get() == false) {
3070           qState.nest.writeSnakes(); // write snPQ file (if any snake) and paQP
3071           saveStats = true; // write also stQP file
3072 
3073         }
3074         // if (qState.nest.writeSnakes()) { // write snPQ file (if any snake) and paQP
3075         // write stQP file and fill outFile used later
3076         List<CellStatsEval> ret =
3077                 qState.nest.analyse(imageGroup.getOrgIpl().duplicate(), saveStats);
3078         // auto save plugin config (but only if there is at least one snake)
3079         if (!qState.nest.isVacant()) {
3080           // Create Serialization object with extra info layer
3081           Serializer<SnakePluginList> s;
3082           s = new Serializer<>(qState.snakePluginList, quimpInfo);
3083           s.setPretty(); // set pretty format
3084           s.save(qState.boap.deductFilterFileName());
3085           s = null; // remove
3086           // Dump BOAState object in new format
3087           DataContainerilesystem/DataContainer.html#DataContainer">DataContainer dt = new DataContainer(); // create container
3088           dt.BOAState = qState; // assign boa state to correct field
3089           // extract relevant data from CellStat
3090           dt.Stats = new StatsCollection();
3091           dt.Stats.copyFromCellStat(ret); // StatsHandler is initialized here.
3092           Serializer<DataContainer> n = new Serializer<>(dt, quimpInfo);
3093           if (qState.boap.savePretty) {
3094             n.setPretty();
3095           }
3096           n.save(qState.boap.deductNewParamFileName());
3097           n = null;
3098           BOA_.log("Updated file " + BOA_.qState.boap.deductNewParamFileName());
3099         } else {
3100           BOA_.log("Nest empty. Nothing saved.");
3101           JOptionPane.showMessageDialog(window,
3102                   QuimpToolsCollection.stringWrap(
3103                           "There are not any cell segmented! Nothing has been saved.",
3104                           QuimP.LINE_WRAP),
3105                   "Info", JOptionPane.INFORMATION_MESSAGE);
3106         }
3107       } catch (IOException e) {
3108         IJ.error("Problem with saving files", e.getMessage());
3109         LOGGER.debug(e.getMessage(), e);
3110         return;
3111       }
3112     }
3113   }
3114 
3115   /**
3116    * Action for Quit button Set BOA_.running static field to false and close the window.
3117    * 
3118    */
3119   void quit() {
3120     BOA_.log("Finish: Exiting BOA...");
3121     imageGroup.makeContourImage();
3122     BOA_.isBoaRunning = false;
3123     qState.nest = null; // remove from memory
3124     imageGroup.getOrgIpl().setOverlay(new Overlay()); // clear overlay
3125     new StackWindow(imageGroup.getOrgIpl());
3126 
3127     window.setImage(new ImagePlus());// remove link to window
3128     window.close();
3129   }
3130 
3131 }
3132 
3133 /**
3134  * Hold, manipulate and draw on images.
3135  * 
3136  */
3137 class ImageGroup {
3138   // paths - snake drawn as it contracts
3139   // contour - final snake drawn
3140   // org - original image, kept clean
3141 
3142   private ImagePlus orgIpl;
3143   private ImagePlus pathsIpl; // , contourIpl;
3144   private ImageStack orgStack;
3145   private ImageStack pathsStack; // , contourStack;
3146   private ImageProcessor orgIp;
3147   private ImageProcessor pathsIp; // , contourIp;
3148   private Overlay overlay;
3149   private Nest nest;
3150   private int iplWidth;
3151   private int iplHeight;
3152   private int iplStack;
3153 
3154   private static final Logger LOGGER = LoggerFactory.getLogger(ImageGroup.class.getName());
3155 
3156   /**
3157    * Constructor.
3158    * 
3159    * @param ipl current image opened in IJ
3160    * @param n Nest object associated with BOA
3161    */
3162   public ImageGroup(ImagePlus ipl, Nest n) {
3163     nest = n;
3164     // create two new stacks for drawing
3165 
3166     // image set up
3167     orgIpl = ipl;
3168     orgIpl.setSlice(1);
3169     orgIpl.getCanvas().unzoom();
3170     orgIpl.getCanvas().getMagnification();
3171 
3172     orgStack = orgIpl.getStack();
3173     orgIp = orgStack.getProcessor(1);
3174 
3175     iplWidth = orgIp.getWidth();
3176     iplHeight = orgIp.getHeight();
3177     iplStack = orgIpl.getStackSize();
3178 
3179     // set up blank path image
3180     pathsIpl = NewImage.createByteImage("Node Paths", iplWidth, iplHeight, iplStack,
3181             NewImage.FILL_BLACK);
3182     pathsStack = pathsIpl.getStack();
3183     pathsIpl.setSlice(1);
3184     pathsIp = pathsStack.getProcessor(1);
3185 
3186     setIpSliceAll(1);
3187     setProcessor(1);
3188   }
3189 
3190   /**
3191    * Sets new Nest object associated with displayed image.
3192    * 
3193    * <p>Used after loading new BOAState
3194    * 
3195    * @param newNest new Nest
3196    */
3197   public void updateNest(Nest newNest) {
3198     nest = newNest;
3199   }
3200 
3201   public ImagePlus getOrgIpl() {
3202     return orgIpl;
3203   }
3204 
3205   /**
3206    * Return IP with drawn paths of snakes.
3207    * 
3208    * @return ImageProcessor with paths
3209    * @see BOAState.SegParam#showPaths
3210    */
3211   public ImagePlus getPathsIpl() {
3212     return pathsIpl;
3213   }
3214 
3215   public ImageProcessor getOrgIp() {
3216     return orgIp;
3217   }
3218 
3219   /**
3220    * Plots snakes on current frame.
3221    * 
3222    * <p>Depending on configuration this method can plot:
3223    * <ol>
3224    * <li>Snake after segmentation, without processing by plugins
3225    * <li>Snake after segmentation and after processing by all active plugins
3226    * </ol>
3227    * It assign also last created Snake to ViewUpdater. This Snake can be accessed by plugin for
3228    * previewing purposes. If last Snake has been deleted, null is assigned or before last Snake
3229    * 
3230    * <p>Used when there is a need of redrawing screen because of new data
3231    * 
3232    * @param frame Current frame
3233    */
3234   public void updateOverlay(int frame) {
3235     LOGGER.trace("Update overlay for frame " + frame);
3236     SnakeHandler snakeH;
3237     Snake snake;
3238     Snake back;
3239     int x;
3240     int y;
3241     TextRoi text;
3242     Roi r;
3243     overlay = new Overlay();
3244     BOA_.viewUpdater.connectSnakeObject(null); //
3245     Color snakeColor = Color.YELLOW;
3246     for (int i = 0; i < nest.size(); i++) {
3247       snakeH = nest.getHandler(i);
3248       if (snakeH.isSnakeHandlerFrozen()) {
3249         snakeColor = Color.BLUE;
3250       } else {
3251         snakeColor = Color.YELLOW;
3252       }
3253       if (snakeH.isStoredAt(frame)) { // is there a snake at iplStack?
3254 
3255         // plot segmented snake
3256         if (BOA_.qState.boap.isProcessedSnakePlotted == true) {
3257           back = snakeH.getBackupSnake(frame); // original unmodified snake
3258           // Roi r = snake.asRoi();
3259           r = back.asFloatRoi();
3260           r.setStrokeColor(Color.RED);
3261           overlay.add(r);
3262         }
3263         // remember instance of segmented snake for plugins (last created)
3264         BOA_.viewUpdater.connectSnakeObject(snakeH.getBackupSnake(frame));
3265         // plot segmented and filtered snake
3266         snake = snakeH.getStoredSnake(frame); // processed by plugins
3267         // Roi r = snake.asRoi();
3268         r = snake.asFloatRoi();
3269         r.setStrokeColor(snakeColor);
3270         overlay.add(r);
3271         x = (int) Math.round(snake.getCentroid().getX()) - 15;
3272         y = (int) Math.round(snake.getCentroid().getY()) - 15;
3273         text = new TextRoi(x, y, "   " + snake.getSnakeID());
3274         overlay.add(text);
3275 
3276         // draw centre point
3277         PointRoi pointR =
3278                 new PointRoi((int) snake.getCentroid().getX(), (int) snake.getCentroid().getY());
3279         overlay.add(pointR);
3280 
3281         // draw head node
3282         if (BOA_.qState.boap.isHeadPlotted == true) {
3283           // base point = 0 node
3284           Point2d bp = new Point2d(snake.getHead().getX(), snake.getHead().getY());
3285 
3286           // Plot Arrow mounted in 0 node and pointing direction of Snake
3287           Vector2d dir =
3288                   new Vector2d(snake.getHead().getNext().getNext().getNext().getX() - bp.getX(),
3289                           snake.getHead().getNext().getNext().getNext().getY() - bp.getY());
3290           FloatPolygon fp = GraphicsElements.plotArrow(dir, bp, 12.0f, 0.3f);
3291           PolygonRoi polygonR = new PolygonRoi(fp, Roi.POLYGON);
3292           polygonR.setStrokeColor(Color.MAGENTA);
3293           polygonR.setFillColor(Color.MAGENTA);
3294           overlay.add(polygonR);
3295 
3296           // plot circle on head
3297           FloatPolygon fp1 = GraphicsElements.getCircle(bp, 10);
3298           PolygonRoi polyginR1 = new PolygonRoi(fp1, Roi.POLYGON);
3299           polyginR1.setStrokeColor(Color.GREEN);
3300           polyginR1.setFillColor(Color.GREEN);
3301           overlay.add(polyginR1);
3302         }
3303       } else {
3304         BOA_.viewUpdater.connectSnakeObject(null);
3305       }
3306     }
3307     orgIpl.setOverlay(overlay);
3308   }
3309 
3310   /**
3311    * Updates IJ to current frame. Causes that updateSliceSelector() is called.
3312    * 
3313    * <p>USed when there is a need to move to other frame programmatically.
3314    * 
3315    * @param frame current frame
3316    * 
3317    */
3318   public void updateToFrame(int frame) {
3319     clearPaths(frame);
3320     setProcessor(frame);
3321     setIpSliceAll(frame);
3322   }
3323 
3324   public void clearOverlay() {
3325     // overlay = new Overlay();
3326     orgIpl.setOverlay(null);
3327   }
3328 
3329   /**
3330    * Set internal field to currently processed ImageProcessor.
3331    * 
3332    * <p>Those fields are used by e.g. {@link BOA_#runBoa(int, int)} during computations.
3333    * 
3334    * @param i current frame
3335    */
3336   public final void setProcessor(int i) {
3337     orgIp = orgStack.getProcessor(i);
3338     pathsIp = pathsStack.getProcessor(i);
3339     // System.out.println("\n1217 Proc set to : " + i);
3340   }
3341 
3342   /**
3343    * Set slice in stack. Call updateSliceSelector callback only if i != current frame.
3344    * 
3345    * @param i slice number
3346    */
3347   public final void setIpSliceAll(int i) {
3348     // set slice on all images
3349     pathsIpl.setSlice(i);
3350     orgIpl.setSlice(i);
3351   }
3352 
3353   /**
3354    * Erase all paths stored during segmentation.
3355    * 
3356    * <p>Paths are drawn in separate internal ImagePorcessot that is then displayed in place of
3357    * original image in BOA.
3358    * 
3359    * @param fromFrame start frame to erase from
3360    * 
3361    * @see #getPathsIpl()
3362    * @see BOAState.SegParam#showPaths
3363    */
3364   public void clearPaths(int fromFrame) {
3365     for (int i = fromFrame; i <= BOA_.qState.boap.getFrames(); i++) {
3366       pathsIp = pathsStack.getProcessor(i);
3367       pathsIp.setValue(0);
3368       pathsIp.fill();
3369     }
3370     pathsIp = pathsStack.getProcessor(fromFrame);
3371   }
3372 
3373   /**
3374    * Draw Snake contraction paths in internal stack.
3375    * 
3376    * <p>Internal stack with paths can be displayed by setting {@link BOAState.SegParam#showPaths}
3377    * 
3378    * @param snake snake to draw
3379    * @param frame current frame
3380    */
3381   public void drawPath(Snake snake, int frame) {
3382     pathsIp = pathsStack.getProcessor(frame);
3383     drawSnake(pathsIp, snake, false);
3384   }
3385 
3386   private void drawSnake(final ImageProcessor ip, final Snake snake, boolean contrast) {
3387     // draw snake
3388     int x;
3389     int y;
3390     int intensity;
3391 
3392     Node n = snake.getHead();
3393     do {
3394       x = (int) (n.getPoint().getX());
3395       y = (int) (n.getPoint().getY());
3396 
3397       if (!contrast) {
3398         intensity = 245;
3399       } else {
3400         // paint as black or white for max contrast
3401         if (ip.getPixel(x, y) > 127) {
3402           intensity = 10;
3403         } else {
3404           intensity = 245;
3405         }
3406       }
3407       // for colour:
3408       // if(boap.drawColor) intensity = n.colour.getColorInt();
3409 
3410       if (BOA_.qState.boap.getHeight() > 800) {
3411         drawPixel(x, y, intensity, true, ip);
3412       } else {
3413         drawPixel(x, y, intensity, false, ip);
3414       }
3415       n = n.getNext();
3416     } while (!n.isHead());
3417   }
3418 
3419   private void drawPixel(int x, int y, int intensity, boolean fat, ImageProcessor ip) {
3420     ip.putPixel(x, y, intensity);
3421     if (fat) {
3422       ip.putPixel(x + 1, y, intensity);
3423       ip.putPixel(x + 1, y + 1, intensity);
3424       ip.putPixel(x, y + 1, intensity);
3425       ip.putPixel(x - 1, y + 1, intensity);
3426       ip.putPixel(x - 1, y, intensity);
3427       ip.putPixel(x - 1, y - 1, intensity);
3428       ip.putPixel(x, y - 1, intensity);
3429       ip.putPixel(x + 1, y - 1, intensity);
3430     }
3431   }
3432 
3433   void makeContourImage() {
3434     ImagePlus contourIpl = NewImage.createByteImage("Contours", iplWidth, iplHeight, iplStack,
3435             NewImage.FILL_BLACK);
3436     ImageStack contourStack = contourIpl.getStack();
3437     contourIpl.setSlice(1);
3438     ImageProcessor contourIp;
3439 
3440     for (int i = 1; i <= BOA_.qState.boap.getFrames(); i++) { // copy original
3441       orgIp = orgStack.getProcessor(i);
3442       contourIp = contourStack.getProcessor(i);
3443       contourIp.copyBits(orgIp, 0, 0, Blitter.COPY);
3444     }
3445 
3446     drawCellRois(contourStack);
3447     new ImagePlus(orgIpl.getTitle() + "_Segmentation", contourStack).show();
3448 
3449   }
3450 
3451   /**
3452    * Zoom current view to snake with snakeID.
3453    * 
3454    * <p>If snake is not found nothing happens.
3455    * 
3456    * @param ic Current view
3457    * @param frame Frame the Snake is looked in
3458    * @param snakeID ID of Snake one looks for
3459    */
3460   void zoom(final ImageCanvas ic, int frame, int snakeID) {
3461     LOGGER.trace("Zoom to frame: " + frame + " ID " + snakeID);
3462     if (nest.isVacant() || snakeID < 0) {
3463       return; // negative id or empty nest
3464     }
3465     SnakeHandler snakeH;
3466     Snake snake;
3467 
3468     try {
3469       snakeH = nest.getHandlerofId(snakeID);// snakeID, not index
3470       if (snakeH != null && snakeH.isStoredAt(frame)) {
3471         snake = snakeH.getStoredSnake(frame);
3472       } else {
3473         return;
3474       }
3475     } catch (IndexOutOfBoundsException | IllegalArgumentException e) {
3476       LOGGER.debug(e.getMessage(), e);
3477       return;
3478     }
3479 
3480     Rectangle r = snake.getBounds();
3481     int border = 40;
3482 
3483     // add border (10 either way)
3484     r.setBounds(r.x - border, r.y - border, r.width + border * 2, r.height + border * 2);
3485 
3486     // correct r's aspect ratio
3487     double icAspect = (double) ic.getWidth() / (double) ic.getHeight();
3488     double rectAspect = r.getWidth() / r.getHeight();
3489     int newDim; // new dimenesion size
3490 
3491     if (icAspect < rectAspect) {
3492       // too short
3493       newDim = (int) (r.getWidth() / icAspect);
3494       r.y = r.y - ((newDim - r.height) / 2); // move snake to center
3495       r.height = newDim;
3496     } else {
3497       // too thin
3498       newDim = (int) (r.getHeight() * icAspect);
3499       r.x = r.x - ((newDim - r.width) / 2); // move snake to center
3500       r.width = newDim;
3501     }
3502 
3503     // System.out.println("ic " + icAspect + ". rA: " + r.getWidth() /
3504     // r.getHeight());
3505 
3506     double newMag;
3507 
3508     newMag = (double) ic.getHeight() / r.getHeight(); // mag required
3509 
3510     ic.setMagnification(newMag);
3511     Rectangle sr = ic.getSrcRect();
3512     sr.setBounds(r);
3513 
3514     ic.repaint();
3515 
3516   }
3517 
3518   void unzoom(final ImageCanvas ic) {
3519     // Rectangle sr = ic.getSrcRect();
3520     // sr.setBounds(0, 0, boap.WIDTH, boap.HEIGHT);
3521     ic.unzoom();
3522     // ic.setMagnification(orgMag);
3523     // ic.repaint();
3524   }
3525 
3526   /**
3527    * Produces final image with Snake outlines after finishing BOA.
3528    * 
3529    * <p>This is ImageJ image with flatten Snake contours.
3530    * 
3531    * @param stack Stack to plot in
3532    */
3533   void drawCellRois(final ImageStack stack) {
3534     Snake snake;
3535     SnakeHandler snakeH;
3536     ImageProcessor ip;
3537 
3538     int x;
3539     int y;
3540     for (int s = 0; s < nest.size(); s++) {
3541       snakeH = nest.getHandler(s);
3542       for (int i = 1; i <= BOA_.qState.boap.getFrames(); i++) {
3543         if (snakeH.isStoredAt(i)) {
3544           snake = snakeH.getStoredSnake(i);
3545           ip = stack.getProcessor(i);
3546           ip.setColor(255);
3547           ip.draw(snake.asFloatRoi());
3548           x = (int) Math.round(snake.getHead().getX()) - 15;
3549           y = (int) Math.round(snake.getHead().getY()) - 15;
3550           ip.moveTo(x, y);
3551           ip.drawString("   " + snake.getSnakeID());
3552           LOGGER.trace("Snake head is at: " + snake.getHead().toString());
3553         }
3554       }
3555     }
3556   }
3557 }