001/*
002 * Copyright 2010-2015 Institut Pasteur.
003 * 
004 * This file is part of Icy.
005 * 
006 * Icy is free software: you can redistribute it and/or modify
007 * it under the terms of the GNU General Public License as published by
008 * the Free Software Foundation, either version 3 of the License, or
009 * (at your option) any later version.
010 * 
011 * Icy is distributed in the hope that it will be useful,
012 * but WITHOUT ANY WARRANTY; without even the implied warranty of
013 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
014 * GNU General Public License for more details.
015 * 
016 * You should have received a copy of the GNU General Public License
017 * along with Icy. If not, see <http://www.gnu.org/licenses/>.
018 */
019package icy.gui.component;
020
021import java.awt.BorderLayout;
022import java.awt.Color;
023import java.awt.Dimension;
024import java.awt.Image;
025import java.awt.Point;
026import java.awt.event.KeyEvent;
027import java.awt.event.MouseAdapter;
028import java.awt.event.MouseEvent;
029import java.lang.ref.WeakReference;
030import java.util.ArrayList;
031import java.util.Collections;
032import java.util.Comparator;
033import java.util.HashMap;
034import java.util.HashSet;
035import java.util.Iterator;
036import java.util.LinkedHashSet;
037import java.util.List;
038import java.util.Map;
039import java.util.Map.Entry;
040import java.util.Set;
041import java.util.concurrent.Semaphore;
042import java.util.concurrent.TimeUnit;
043
044import javax.swing.ActionMap;
045import javax.swing.Box;
046import javax.swing.DefaultListSelectionModel;
047import javax.swing.InputMap;
048import javax.swing.JComponent;
049import javax.swing.JLabel;
050import javax.swing.JPanel;
051import javax.swing.JScrollPane;
052import javax.swing.KeyStroke;
053import javax.swing.ListSelectionModel;
054import javax.swing.ScrollPaneConstants;
055import javax.swing.event.ListSelectionEvent;
056import javax.swing.event.ListSelectionListener;
057import javax.swing.table.AbstractTableModel;
058import javax.swing.table.JTableHeader;
059import javax.swing.table.TableColumn;
060import javax.swing.table.TableColumnModel;
061import javax.swing.table.TableModel;
062
063import org.jdesktop.swingx.JXTable;
064import org.jdesktop.swingx.decorator.HighlighterFactory;
065import org.jdesktop.swingx.sort.DefaultSortController;
066import org.jdesktop.swingx.table.DefaultTableColumnModelExt;
067import org.jdesktop.swingx.table.TableColumnExt;
068import org.pushingpixels.substance.api.renderers.SubstanceDefaultTableCellRenderer;
069import org.pushingpixels.substance.api.skin.SkinChangeListener;
070
071import icy.action.RoiActions;
072import icy.canvas.IcyCanvas;
073import icy.canvas.IcyCanvas2D;
074import icy.canvas.IcyCanvas3D;
075import icy.gui.component.IcyTextField.TextChangeListener;
076import icy.gui.component.button.IcyButton;
077import icy.gui.component.renderer.ImageTableCellRenderer;
078import icy.gui.inspector.RoiSettingFrame;
079import icy.gui.main.ActiveSequenceListener;
080import icy.gui.util.GuiUtil;
081import icy.gui.util.LookAndFeelUtil;
082import icy.gui.viewer.Viewer;
083import icy.main.Icy;
084import icy.math.MathUtil;
085import icy.plugin.PluginLoader;
086import icy.plugin.PluginLoader.PluginLoaderEvent;
087import icy.plugin.PluginLoader.PluginLoaderListener;
088import icy.plugin.interface_.PluginROIDescriptor;
089import icy.preferences.XMLPreferences;
090import icy.roi.ROI;
091import icy.roi.ROIDescriptor;
092import icy.roi.ROIEvent;
093import icy.roi.ROIEvent.ROIEventType;
094import icy.roi.ROIListener;
095import icy.roi.ROIUtil;
096import icy.sequence.Sequence;
097import icy.sequence.SequenceEvent;
098import icy.sequence.SequenceEvent.SequenceEventSourceType;
099import icy.system.IcyExceptionHandler;
100import icy.system.thread.InstanceProcessor;
101import icy.system.thread.ThreadUtil;
102import icy.type.rectangle.Rectangle5D;
103import icy.util.ClassUtil;
104import icy.util.StringUtil;
105import plugins.kernel.roi.descriptor.intensity.ROIMaxIntensityDescriptor;
106import plugins.kernel.roi.descriptor.intensity.ROIMeanIntensityDescriptor;
107import plugins.kernel.roi.descriptor.intensity.ROIMinIntensityDescriptor;
108import plugins.kernel.roi.descriptor.intensity.ROISumIntensityDescriptor;
109import plugins.kernel.roi.descriptor.measure.ROIAreaDescriptor;
110import plugins.kernel.roi.descriptor.measure.ROIContourDescriptor;
111import plugins.kernel.roi.descriptor.measure.ROIInteriorDescriptor;
112import plugins.kernel.roi.descriptor.measure.ROIMassCenterCDescriptor;
113import plugins.kernel.roi.descriptor.measure.ROIMassCenterTDescriptor;
114import plugins.kernel.roi.descriptor.measure.ROIMassCenterXDescriptor;
115import plugins.kernel.roi.descriptor.measure.ROIMassCenterYDescriptor;
116import plugins.kernel.roi.descriptor.measure.ROIMassCenterZDescriptor;
117import plugins.kernel.roi.descriptor.measure.ROIPerimeterDescriptor;
118import plugins.kernel.roi.descriptor.measure.ROISurfaceAreaDescriptor;
119import plugins.kernel.roi.descriptor.measure.ROIVolumeDescriptor;
120import plugins.kernel.roi.descriptor.property.ROIColorDescriptor;
121import plugins.kernel.roi.descriptor.property.ROIGroupIdDescriptor;
122import plugins.kernel.roi.descriptor.property.ROIIconDescriptor;
123import plugins.kernel.roi.descriptor.property.ROINameDescriptor;
124import plugins.kernel.roi.descriptor.property.ROIOpacityDescriptor;
125import plugins.kernel.roi.descriptor.property.ROIPositionCDescriptor;
126import plugins.kernel.roi.descriptor.property.ROIPositionTDescriptor;
127import plugins.kernel.roi.descriptor.property.ROIPositionXDescriptor;
128import plugins.kernel.roi.descriptor.property.ROIPositionYDescriptor;
129import plugins.kernel.roi.descriptor.property.ROIPositionZDescriptor;
130import plugins.kernel.roi.descriptor.property.ROISizeCDescriptor;
131import plugins.kernel.roi.descriptor.property.ROISizeTDescriptor;
132import plugins.kernel.roi.descriptor.property.ROISizeXDescriptor;
133import plugins.kernel.roi.descriptor.property.ROISizeYDescriptor;
134import plugins.kernel.roi.descriptor.property.ROISizeZDescriptor;
135
136/**
137 * Abstract ROI panel component
138 */
139public abstract class AbstractRoisPanel extends ExternalizablePanel
140        implements ActiveSequenceListener, TextChangeListener, ListSelectionListener, PluginLoaderListener
141{
142    /**
143     * 
144     */
145    protected static final long serialVersionUID = -2870878233087117178L;
146
147    protected static final String ID_VIEW = "view";
148    protected static final String ID_EXPORT = "export";
149
150    protected static final String ID_PROPERTY_MINSIZE = "minSize";
151    protected static final String ID_PROPERTY_MAXSIZE = "maxSize";
152    protected static final String ID_PROPERTY_DEFAULTSIZE = "defaultSize";
153    protected static final String ID_PROPERTY_ORDER = "order";
154    protected static final String ID_PROPERTY_VISIBLE = "visible";
155
156    // default row comparator
157    protected static Comparator<Object> comparator = new Comparator<Object>()
158    {
159        @SuppressWarnings({"unchecked", "rawtypes"})
160        @Override
161        public int compare(Object o1, Object o2)
162        {
163            if (o1 == null)
164            {
165                if (o2 == null)
166                    return 0;
167                return -1;
168            }
169            if (o2 == null)
170                return 1;
171
172            Object obj1 = o1;
173            Object obj2 = o2;
174
175            if (o1 instanceof String)
176            {
177                if (o1.equals("-" + MathUtil.INFINITE_STRING))
178                    obj1 = Double.valueOf(Double.NEGATIVE_INFINITY);
179                else if (o1.equals(MathUtil.INFINITE_STRING))
180                    obj1 = Double.valueOf(Double.POSITIVE_INFINITY);
181            }
182
183            if (o2 instanceof String)
184            {
185                if (o2.equals("-" + MathUtil.INFINITE_STRING))
186                    obj2 = Double.valueOf(Double.NEGATIVE_INFINITY);
187                else if (o2.equals(MathUtil.INFINITE_STRING))
188                    obj2 = Double.valueOf(Double.POSITIVE_INFINITY);
189            }
190
191            if ((obj1 instanceof Number) && (obj2 instanceof Number))
192            {
193                final double d1 = ((Number) obj1).doubleValue();
194                final double d2 = ((Number) obj2).doubleValue();
195
196                if (Double.isNaN(d1))
197                {
198                    if (Double.isNaN(d2))
199                        return 0;
200                    return -1;
201                }
202                if (Double.isNaN(d2))
203                    return 1;
204
205                if (d1 < d2)
206                    return -1;
207                if (d1 > d2)
208                    return 1;
209
210                return 0;
211            }
212            else if ((obj1 instanceof Comparable) && (obj1.getClass() == obj2.getClass()))
213                return ((Comparable) obj1).compareTo(obj2);
214
215            return o1.toString().compareTo(o2.toString());
216        }
217    };
218
219    // GUI
220    protected ROITableModel roiTableModel;
221    protected ListSelectionModel roiSelectionModel;
222    protected JXTable roiTable;
223    protected IcyTextField nameFilter;
224    protected JLabel roiNumberLabel;
225    protected JLabel selectedRoiNumberLabel;
226
227    // PluginDescriptors / ROIDescriptor map
228    protected Map<ROIDescriptor, PluginROIDescriptor> descriptorMap;
229    // DescriptorComputer / ROIDescriptor map
230    protected Map<ROIDescriptor, DescriptorComputer> descriptorComputerMap;
231
232    // Descriptor / column info (static to the class)
233    protected List<ColumnInfo> columnInfoList;
234    // // last visible columns (used to detect change in column configuration)
235    // List<String> lastVisibleColumnIds;
236
237    // ROI info list cache
238    protected Set<ROI> roiSet;
239    protected Map<ROI, ROIResults> roiResultsMap;
240    protected List<ROI> filteredRoiList;
241    protected List<ROIResults> filteredRoiResultsList;
242
243    // internals
244    protected final XMLPreferences basePreferences;
245    protected final XMLPreferences viewPreferences;
246    protected final XMLPreferences exportPreferences;
247    protected final Semaphore modifySelection;
248
249    // complete refresh of the roiTable
250    protected final Runnable roiListRefresher;
251    protected final Runnable filteredRoiListRefresher;
252    protected final Runnable tableDataStructureRefresher;
253    protected final Runnable tableDataRefresher;
254    protected final Runnable tableSelectionRefresher;
255    protected final Runnable columnInfoListRefresher;
256    protected final InstanceProcessor processor;
257
258    protected final DescriptorComputer primaryDescriptorComputer;
259    protected final DescriptorComputer basicDescriptorComputer;
260    protected final DescriptorComputer advancedDescriptorComputer;
261
262    protected long lastTableDataRefresh;
263
264    /**
265     * Create a new ROI table panel.<br>
266     * 
267     * @param preferences
268     *        XML preferences node which will contains the ROI table settings
269     */
270    public AbstractRoisPanel(XMLPreferences preferences)
271    {
272        super("ROI", "roiPanel", new Point(100, 100), new Dimension(400, 600));
273
274        basePreferences = preferences;
275        viewPreferences = basePreferences.node(ID_VIEW);
276        exportPreferences = basePreferences.node(ID_EXPORT);
277
278        roiSet = new HashSet<ROI>();
279        roiResultsMap = new HashMap<ROI, ROIResults>();
280        filteredRoiList = new ArrayList<ROI>();
281        filteredRoiResultsList = new ArrayList<ROIResults>();
282        modifySelection = new Semaphore(1);
283        columnInfoList = new ArrayList<ColumnInfo>();
284
285        lastTableDataRefresh = 0L;
286
287        initialize();
288
289        roiListRefresher = new Runnable()
290        {
291            @Override
292            public void run()
293            {
294                refreshRoisInternal();
295            }
296        };
297        filteredRoiListRefresher = new Runnable()
298        {
299            @Override
300            public void run()
301            {
302                refreshFilteredRoisInternal();
303            }
304        };
305        tableDataStructureRefresher = new Runnable()
306        {
307            @Override
308            public void run()
309            {
310                refreshTableDataStructureInternal();
311            }
312        };
313        tableDataRefresher = new Runnable()
314        {
315            @Override
316            public void run()
317            {
318                refreshTableDataInternal();
319            }
320        };
321        tableSelectionRefresher = new Runnable()
322        {
323            @Override
324            public void run()
325            {
326                refreshTableSelectionInternal();
327            }
328        };
329        columnInfoListRefresher = new Runnable()
330        {
331            @Override
332            public void run()
333            {
334                refreshColumnInfoListInternal();
335            }
336        };
337
338        LookAndFeelUtil.addSkinChangeListener(new SkinChangeListener()
339        {
340            @Override
341            public void skinChanged()
342            {
343                // regenerate column model to redefine the Cell Renderer (else colors are wrong)
344                roiTable.setColumnModel(new ROITableColumnModel());
345
346                // final TableColumnModel columnModel = roiTable.getColumnModel();
347                //
348                // for (int i = 0; i < columnModel.getColumnCount(); i++)
349                // {
350                // final TableColumn column = columnModel.getColumn(i);
351                //
352                // // need to reset specific renderer as background color can be wrong
353                // if (column.getCellRenderer() instanceof ImageTableCellRenderer)
354                // column.setCellRenderer(new ImageTableCellRenderer(18));
355                // }
356
357                // modify highlighter
358                roiTable.setHighlighters(HighlighterFactory.createSimpleStriping());
359            }
360        });
361
362        processor = new InstanceProcessor();
363        processor.setThreadName("ROI panel GUI refresher");
364        processor.setKeepAliveTime(30, TimeUnit.SECONDS);
365
366        primaryDescriptorComputer = new DescriptorComputer(DescriptorType.PRIMARY);
367        basicDescriptorComputer = new DescriptorComputer(DescriptorType.BASIC);
368        advancedDescriptorComputer = new DescriptorComputer(DescriptorType.EXTERNAL);
369        primaryDescriptorComputer.start();
370        basicDescriptorComputer.start();
371        advancedDescriptorComputer.start();
372
373        // update descriptors list (this rebuild the column model of the tree table)
374        refreshDescriptorList();
375        // set shortcuts
376        buildActionMap();
377
378        refreshRois();
379
380        // listen plugin loader changes
381        PluginLoader.addListener(this);
382    }
383
384    protected void initialize()
385    {
386        // need filter before load
387        nameFilter = new IcyTextField();
388        nameFilter.setToolTipText("Filter ROI by name");
389        nameFilter.addTextChangeListener(this);
390
391        selectedRoiNumberLabel = new JLabel("0");
392        roiNumberLabel = new JLabel("0");
393
394        // build roiTable model
395        roiTableModel = new ROITableModel();
396
397        // build roiTable
398        roiTable = new JXTable(roiTableModel);
399        roiTable.setAutoStartEditOnKeyStroke(false);
400        roiTable.setAutoCreateRowSorter(false);
401        roiTable.setAutoCreateColumnsFromModel(false);
402        roiTable.setShowVerticalLines(false);
403        roiTable.setColumnControlVisible(false);
404        roiTable.setColumnSelectionAllowed(false);
405        roiTable.setRowSelectionAllowed(true);
406        roiTable.setSortable(true);
407        // set highlight
408        roiTable.setHighlighters(HighlighterFactory.createSimpleStriping());
409        roiTable.addMouseListener(new MouseAdapter()
410        {
411            @Override
412            public void mousePressed(MouseEvent event)
413            {
414                if (event.getClickCount() == 2)
415                {
416                    final int c = roiTable.columnAtPoint(event.getPoint());
417                    TableColumn col = null;
418
419                    if (c != -1)
420                        col = roiTable.getColumn(c);
421
422                    if ((col == null) || !col.getHeaderValue().equals(new ROINameDescriptor().getName()))
423                        roiTableDoubleClicked();
424                }
425            }
426        });
427
428        // set header settings
429        final JTableHeader tableHeader = roiTable.getTableHeader();
430        tableHeader.setReorderingAllowed(false);
431        tableHeader.setResizingAllowed(true);
432
433        // set selection model
434        roiSelectionModel = roiTable.getSelectionModel();
435        roiSelectionModel.addListSelectionListener(this);
436        roiSelectionModel.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
437
438        roiTable.setRowSorter(new ROITableSortController<ROITableModel>());
439
440        final JPanel middlePanel = new JPanel(new BorderLayout(0, 0));
441
442        middlePanel.add(roiTable.getTableHeader(), BorderLayout.NORTH);
443        middlePanel.add(new JScrollPane(roiTable, ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS,
444                ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER), BorderLayout.CENTER);
445
446        final IcyButton settingButton = new IcyButton(RoiActions.settingAction);
447        settingButton.setHideActionText(true);
448        settingButton.setFlat(true);
449
450        final IcyButton xlsExportButton = new IcyButton(RoiActions.xlsExportAction);
451        xlsExportButton.setHideActionText(true);
452        xlsExportButton.setFlat(true);
453
454        setLayout(new BorderLayout());
455        add(GuiUtil.createLineBoxPanel(nameFilter, Box.createHorizontalStrut(8), selectedRoiNumberLabel,
456                new JLabel(" / "), roiNumberLabel, Box.createHorizontalStrut(4), settingButton, xlsExportButton),
457                BorderLayout.NORTH);
458        add(middlePanel, BorderLayout.CENTER);
459
460        validate();
461    }
462
463    protected void buildActionMap()
464    {
465        final InputMap imap = roiTable.getInputMap(JComponent.WHEN_FOCUSED);
466        final ActionMap amap = roiTable.getActionMap();
467
468        imap.put(RoiActions.unselectAction.getKeyStroke(), RoiActions.unselectAction.getName());
469        imap.put(RoiActions.deleteAction.getKeyStroke(), RoiActions.deleteAction.getName());
470        // also allow backspace key for delete operation here
471        imap.put(KeyStroke.getKeyStroke(KeyEvent.VK_BACK_SPACE, 0), RoiActions.deleteAction.getName());
472        imap.put(RoiActions.copyAction.getKeyStroke(), RoiActions.copyAction.getName());
473        imap.put(RoiActions.pasteAction.getKeyStroke(), RoiActions.pasteAction.getName());
474        imap.put(RoiActions.copyLinkAction.getKeyStroke(), RoiActions.copyLinkAction.getName());
475        imap.put(RoiActions.pasteLinkAction.getKeyStroke(), RoiActions.pasteLinkAction.getName());
476
477        // disable search feature (we have our own filter)
478        amap.remove("find");
479        amap.put(RoiActions.unselectAction.getName(), RoiActions.unselectAction);
480        amap.put(RoiActions.deleteAction.getName(), RoiActions.deleteAction);
481        amap.put(RoiActions.copyAction.getName(), RoiActions.copyAction);
482        amap.put(RoiActions.pasteAction.getName(), RoiActions.pasteAction);
483        amap.put(RoiActions.copyLinkAction.getName(), RoiActions.copyLinkAction);
484        amap.put(RoiActions.pasteLinkAction.getName(), RoiActions.pasteLinkAction);
485    }
486
487    protected ROIResults createNewROIResults(ROI roi)
488    {
489        return new ROIResults(roi);
490    }
491
492    /**
493     * Returns number of channel of current sequence
494     */
495    protected int getChannelCount()
496    {
497        final Sequence sequence = getSequence();
498
499        if (sequence != null)
500            return sequence.getSizeC();
501
502        return 1;
503    }
504
505    /**
506     * Returns roiTable column suffix for the specified channel
507     */
508    protected String getChannelNameSuffix(int ch)
509    {
510        final Sequence sequence = getSequence();
511
512        if ((sequence != null) && (ch < getChannelCount()))
513            return " (" + sequence.getChannelName(ch) + ")";
514
515        return "";
516    }
517
518    /**
519     * Returns ROI descriptor given its id.
520     */
521    protected ROIDescriptor getROIDescriptor(String descriptorId)
522    {
523        final ROIDescriptor[] descriptors;
524
525        synchronized (descriptorMap)
526        {
527            descriptors = descriptorMap.keySet().toArray(new ROIDescriptor[descriptorMap.size()]);
528        }
529
530        for (ROIDescriptor descriptor : descriptors)
531            if (descriptor.getId().equals(descriptorId))
532                return descriptor;
533
534        return null;
535    }
536
537    // /**
538    // * Get column info for specified visible column index.
539    // */
540    // protected ColumnInfo getVisibleColumnInfo(List<ColumnInfo> columns, int column)
541    // {
542    // int ind = 0;
543    // for (int c = 0; c < columns.size(); c++)
544    // {
545    // final ColumnInfo col = columns.get(c);
546    //
547    // if (col.visible)
548    // {
549    // if (ind == column)
550    // return col;
551    // ind++;
552    // }
553    // }
554    //
555    // return null;
556    // }
557    //
558    // /**
559    // * Get column info for specified visible column index.
560    // */
561    // protected ColumnInfo getVisibleColumnInfo(int column)
562    // {
563    // return getVisibleColumnInfo(columnInfoList, column);
564    // }
565
566    /**
567     * Get column info for specified column index.
568     */
569    protected ColumnInfo getColumnInfo(List<ColumnInfo> columns, int column)
570    {
571        if (column < columns.size())
572            return columns.get(column);
573
574        return null;
575    }
576
577    /**
578     * Get column info for specified column index.
579     */
580    protected ColumnInfo getColumnInfo(int column)
581    {
582        return getColumnInfo(columnInfoList, column);
583    }
584
585    protected ColumnInfo getColumnInfo(List<ColumnInfo> columns, ROIDescriptor descriptor, int channel)
586    {
587        for (ColumnInfo ci : columns)
588            if (ci.descriptor.equals(descriptor) && (ci.channel == channel))
589                return ci;
590
591        return null;
592    }
593
594    protected ColumnInfo getColumnInfo(ROIDescriptor descriptor, int channel)
595    {
596        return getColumnInfo(columnInfoList, descriptor, channel);
597    }
598
599    protected abstract Sequence getSequence();
600
601    public void setNameFilter(String name)
602    {
603        nameFilter.setText(name);
604    }
605
606    protected boolean computeROIResults(ROIResults roiResults, Sequence seq, ColumnInfo columnInfo)
607    {
608        final Map<ColumnInfo, DescriptorResult> results = roiResults.descriptorResults;
609        final ROIDescriptor descriptor = columnInfo.descriptor;
610        final DescriptorResult result;
611
612        synchronized (results)
613        {
614            // get result
615            result = results.get(columnInfo);
616        }
617
618        // need to refresh this column result
619        if ((result != null) && result.isOutdated())
620        {
621            // get the corresponding plugin
622            final PluginROIDescriptor plugin;
623
624            synchronized (descriptorMap)
625            {
626                plugin = descriptorMap.get(descriptor);
627            }
628
629            if (plugin != null)
630            {
631                final Map<ROIDescriptor, Object> newResults;
632
633                try
634                {
635                    // need computation per channel ?
636                    if (descriptor.separateChannel())
637                    {
638                        // retrieve the ROI for this channel
639                        final ROI roi = roiResults.getRoiForChannel(columnInfo.channel);
640
641                        if (roi == null)
642                            throw new UnsupportedOperationException(
643                                    "Can't retrieve sub ROI for channel " + columnInfo.channel);
644
645                        newResults = plugin.compute(roi, seq);
646                    }
647                    else
648                        newResults = plugin.compute(roiResults.roi, seq);
649
650                    for (Entry<ROIDescriptor, Object> entryNewResult : newResults.entrySet())
651                    {
652                        // get the column for this result
653                        final ColumnInfo resultColumnInfo = getColumnInfo(entryNewResult.getKey(), columnInfo.channel);
654                        final DescriptorResult oResult;
655
656                        synchronized (results)
657                        {
658                            // get corresponding result
659                            oResult = results.get(resultColumnInfo);
660                        }
661
662                        if (oResult != null)
663                        {
664                            // set the result value
665                            oResult.setValue(entryNewResult.getValue());
666                            // result is up to date
667                            oResult.setOutdated(false);
668                        }
669                    }
670                }
671                catch (Throwable t)
672                {
673                    // not an UnsupportedOperationException --> show the error
674                    if (!(t instanceof UnsupportedOperationException))
675                        IcyExceptionHandler.handleException(t, true);
676
677                    final List<ROIDescriptor> descriptors = plugin.getDescriptors();
678
679                    if (descriptors != null)
680                    {
681                        // not supported --> clear associated results and set them as computed
682                        for (ROIDescriptor desc : descriptors)
683                        {
684                            // get the column for this result
685                            final ColumnInfo resultColumnInfo = getColumnInfo(desc, columnInfo.channel);
686                            final DescriptorResult oResult;
687
688                            synchronized (results)
689                            {
690                                // get corresponding result
691                                oResult = results.get(resultColumnInfo);
692                            }
693
694                            if (oResult != null)
695                            {
696                                oResult.setValue(null);
697                                oResult.setOutdated(false);
698                            }
699                        }
700                    }
701                }
702
703                // we updated result
704                return true;
705            }
706        }
707
708        return false;
709    }
710
711    /**
712     * Return index of specified ROI in the filtered ROI list
713     */
714    protected int getRoiIndex(ROI roi)
715    {
716        final int result = Collections.binarySearch(filteredRoiList, roi, ROI.idComparator);
717
718        if (result >= 0)
719            return result;
720
721        return -1;
722    }
723
724    /**
725     * Return index of specified ROI in the model
726     */
727    protected int getRoiModelIndex(ROI roi)
728    {
729        return getRoiIndex(roi);
730    }
731
732    /**
733     * Return index of specified ROI in the table (view)
734     */
735    protected int getRoiViewIndex(ROI roi)
736    {
737        final int ind = getRoiModelIndex(roi);
738
739        if (ind == -1)
740            return ind;
741
742        try
743        {
744            return roiTable.convertRowIndexToView(ind);
745        }
746        catch (IndexOutOfBoundsException e)
747        {
748            return -1;
749        }
750    }
751
752    protected ROIResults getRoiResults(int rowModelIndex)
753    {
754        final List<ROIResults> entries = filteredRoiResultsList;
755
756        if ((rowModelIndex >= 0) && (rowModelIndex < entries.size()))
757            return entries.get(rowModelIndex);
758
759        return null;
760    }
761
762    /**
763     * Returns the visible ROI in the ROI control panel.
764     */
765    public List<ROI> getVisibleRois()
766    {
767        return new ArrayList<ROI>(filteredRoiList);
768    }
769
770    // /**
771    // * Returns the ROI informations for the specified ROI.
772    // */
773    // public ROIInfo getROIInfo(ROI roi)
774    // {
775    // final int index = getRoiIndex(roi);
776    //
777    // if (index != -1)
778    // return filteredRois.get(index);
779    //
780    // return null;
781    // }
782
783    /**
784     * Returns the number of selected ROI from the table.
785     */
786    public int getSelectedRoisCount()
787    {
788        int result = 0;
789
790        synchronized (roiSelectionModel)
791        {
792            if (!roiSelectionModel.isSelectionEmpty())
793            {
794                for (int i = roiSelectionModel.getMinSelectionIndex(); i <= roiSelectionModel
795                        .getMaxSelectionIndex(); i++)
796                    if (roiSelectionModel.isSelectedIndex(i))
797                        result++;
798            }
799        }
800
801        return result;
802    }
803
804    /**
805     * Returns the selected ROI from the table.
806     */
807    public List<ROI> getSelectedRois()
808    {
809        final List<ROIResults> roiResults = filteredRoiResultsList;
810        final List<ROI> result = new ArrayList<ROI>(roiResults.size());
811
812        synchronized (roiSelectionModel)
813        {
814            if (!roiSelectionModel.isSelectionEmpty())
815            {
816                for (int i = roiSelectionModel.getMinSelectionIndex(); i <= roiSelectionModel
817                        .getMaxSelectionIndex(); i++)
818                {
819                    if (roiSelectionModel.isSelectedIndex(i))
820                    {
821                        try
822                        {
823                            final int index = roiTable.convertRowIndexToModel(i);
824
825                            if ((index >= 0) && (index < roiResults.size()))
826                                result.add(roiResults.get(index).roi);
827                        }
828                        catch (IndexOutOfBoundsException e)
829                        {
830                            // ignore
831                        }
832                    }
833                }
834            }
835        }
836
837        return result;
838    }
839
840    /**
841     * Select the specified list of ROI in the ROI Table
842     */
843    protected void setSelectedRoisInternal(Set<ROI> newSelected)
844    {
845        final List<Integer> selectedIndexes = new ArrayList<Integer>();
846        final List<ROI> roiList = filteredRoiList;
847
848        for (int i = 0; i < roiList.size(); i++)
849        {
850            final ROI roi = roiList.get(i);
851
852            // HashSet provides fast "contains"
853            if (newSelected.contains(roi))
854            {
855                int ind;
856
857                try
858                {
859                    // convert model index to view index
860                    ind = roiTable.convertRowIndexToView(i);
861                }
862                catch (IndexOutOfBoundsException e)
863                {
864                    ind = -1;
865                }
866
867                if (ind > -1)
868                    selectedIndexes.add(Integer.valueOf(ind));
869            }
870        }
871
872        synchronized (roiSelectionModel)
873        {
874            // start selection change
875            roiSelectionModel.setValueIsAdjusting(true);
876            try
877            {
878                // start by clearing selection
879                roiSelectionModel.clearSelection();
880
881                for (Integer index : selectedIndexes)
882                    roiSelectionModel.addSelectionInterval(index.intValue(), index.intValue());
883            }
884            finally
885            {
886                // end selection change
887                roiSelectionModel.setValueIsAdjusting(false);
888            }
889        }
890    }
891
892    protected Set<ROI> getFilteredSet(String filter)
893    {
894        final Set<ROI> rois = roiSet;
895        final Set<ROI> result = new HashSet<ROI>();
896
897        if (StringUtil.isEmpty(filter, true))
898            result.addAll(rois);
899        else
900        {
901            final String text = filter.trim().toLowerCase();
902
903            // filter on name
904            for (ROI roi : rois)
905                if (roi.getName().toLowerCase().indexOf(text) != -1)
906                    result.add(roi);
907        }
908
909        return result;
910    }
911
912    /**
913     * Display the roi in the table (scroll if needed)
914     */
915    public void scrollTo(ROI roi)
916    {
917        final int index = getRoiIndex(roi);
918
919        if (index != -1)
920            roiTable.scrollRowToVisible(index);
921    }
922
923    protected void refreshRoiNumbers()
924    {
925        final int selectedCount = getSelectedRoisCount();
926        final int roisCount = roiTable.getRowCount();
927
928        selectedRoiNumberLabel.setText(Integer.toString(selectedCount));
929        roiNumberLabel.setText(Integer.toString(roisCount));
930
931        if (selectedCount == 0)
932            selectedRoiNumberLabel.setToolTipText("No selected ROI");
933        else if (selectedCount == 1)
934            selectedRoiNumberLabel.setToolTipText("1 selected ROI");
935        else
936            selectedRoiNumberLabel.setToolTipText(selectedCount + " selected ROIs");
937
938        if (roisCount == 0)
939            roiNumberLabel.setToolTipText("No ROI");
940        else if (roisCount == 1)
941            roiNumberLabel.setToolTipText("1 ROI");
942        else
943            roiNumberLabel.setToolTipText(roisCount + " ROIs");
944    }
945
946    /**
947     * refresh whole ROI list
948     */
949    protected void refreshRois()
950    {
951        processor.submit(true, roiListRefresher);
952    }
953
954    /**
955     * refresh whole ROI list (internal)
956     */
957    protected void refreshRoisInternal()
958    {
959        final Set<ROI> currentRoiSet = roiSet;
960        final Set<ROI> newRoiSet;
961        final Sequence sequence = getSequence();
962
963        if (sequence != null)
964            newRoiSet = sequence.getROISet();
965        else
966            newRoiSet = new HashSet<ROI>();
967
968        // no change --> exit
969        if (newRoiSet.equals(currentRoiSet))
970            return;
971
972        final Set<ROI> removedSet = new HashSet<ROI>();
973
974        // build removed set
975        for (ROI roi : currentRoiSet)
976            if (!newRoiSet.contains(roi))
977                removedSet.add(roi);
978
979        // remove from ROI entry map
980        for (ROI roi : removedSet)
981        {
982            final ROIResults roiResults;
983
984            // must be synchronized
985            synchronized (roiResultsMap)
986            {
987                roiResults = roiResultsMap.remove(roi);
988            }
989
990            // cancel results computation
991            if (roiResults != null)
992                cancelDescriptorComputation(roiResults);
993        }
994
995        // set new ROI set
996        roiSet = newRoiSet;
997
998        // refresh filtered list now
999        refreshFilteredRoisInternal();
1000    }
1001
1002    /**
1003     * refresh filtered ROI list
1004     */
1005    protected void refreshFilteredRois()
1006    {
1007        processor.submit(true, filteredRoiListRefresher);
1008    }
1009
1010    /**
1011     * refresh filtered ROI list (internal)
1012     */
1013    protected void refreshFilteredRoisInternal()
1014    {
1015        // get new filtered list
1016        final List<ROI> currentFilteredRoiList = filteredRoiList;
1017        final Set<ROI> newFilteredRoiSet = getFilteredSet(nameFilter.getText());
1018
1019        // no change --> exit
1020        if (newFilteredRoiSet.equals(currentFilteredRoiList))
1021            return;
1022
1023        // update filtered lists
1024        final List<ROI> newFilteredRoiList = new ArrayList<ROI>(newFilteredRoiSet);
1025        final List<ROIResults> newFilteredResultsList = new ArrayList<ROIResults>(newFilteredRoiList.size());
1026
1027        // sort on id
1028        Collections.sort(newFilteredRoiList, ROI.idComparator);
1029        // then build filtered results list
1030        for (ROI roi : newFilteredRoiList)
1031        {
1032            ROIResults roiResults;
1033
1034            synchronized (roiResultsMap)
1035            {
1036                // try to get the ROI results from the map first
1037                roiResults = roiResultsMap.get(roi);
1038                // and create it if needed
1039                if (roiResults == null)
1040                {
1041                    roiResults = createNewROIResults(roi);
1042                    roiResultsMap.put(roi, roiResults);
1043                }
1044            }
1045
1046            newFilteredResultsList.add(roiResults);
1047        }
1048
1049        filteredRoiList = newFilteredRoiList;
1050        filteredRoiResultsList = newFilteredResultsList;
1051
1052        // update the table model (should always correspond to the filtered roi results list)
1053        refreshTableDataStructureInternal();
1054    }
1055
1056    public void refreshTableDataStructure()
1057    {
1058        processor.submit(true, tableDataStructureRefresher);
1059    }
1060
1061    protected void refreshTableDataStructureInternal()
1062    {
1063        // don't eat too much time on data structure refresh
1064        ThreadUtil.sleep(1);
1065
1066        final Set<ROI> newSelectedRois;
1067        final Sequence sequence = getSequence();
1068
1069        if (sequence != null)
1070            newSelectedRois = sequence.getSelectedROISet();
1071        else
1072            newSelectedRois = new HashSet<ROI>();
1073
1074        ThreadUtil.invokeNow(new Runnable()
1075        {
1076            @Override
1077            public void run()
1078            {
1079                modifySelection.acquireUninterruptibly();
1080                try
1081                {
1082                    synchronized (roiTableModel)
1083                    {
1084                        try
1085                        {
1086                            // notify table data changed
1087                            roiTableModel.fireTableDataChanged();
1088                        }
1089                        catch (Exception e)
1090                        {
1091                            // Sorter don't like when we change data while it's sorting...
1092                        }
1093                    }
1094
1095                    // selection to restore ?
1096                    if (!newSelectedRois.isEmpty())
1097                        setSelectedRoisInternal(newSelectedRois);
1098
1099                    // // force loading values on sorted column
1100                    // final List<? extends SortKey> keys = roiTable.getRowSorter().getSortKeys();
1101                    // if (!keys.isEmpty())
1102                    // forceComputationForColumn(keys.get(0).getColumn());
1103                }
1104                finally
1105                {
1106                    modifySelection.release();
1107                }
1108            }
1109        });
1110
1111        refreshRoiNumbers();
1112    }
1113
1114    public void refreshTableData()
1115    {
1116        processor.submit(true, tableDataRefresher);
1117    }
1118
1119    protected void refreshTableDataInternal()
1120    {
1121        final long time = System.currentTimeMillis();
1122        final boolean hasPendingTask = primaryDescriptorComputer.hasPendingComputation()
1123                && basicDescriptorComputer.hasPendingComputation()
1124                && advancedDescriptorComputer.hasPendingComputation();
1125
1126        // still pending descriptor task ?
1127        if (hasPendingTask)
1128        {
1129            // avoid too much table data update
1130            if ((time - lastTableDataRefresh) < 200)
1131                return;
1132        }
1133
1134        lastTableDataRefresh = time;
1135
1136        // don't eat too much time on data structure refresh
1137        ThreadUtil.sleep(1);
1138
1139        ThreadUtil.invokeNow(new Runnable()
1140        {
1141            @Override
1142            public void run()
1143            {
1144                final int rowCount = roiTable.getRowCount();
1145
1146                // we use 'RowsUpdated' event to keep selection (DataChanged remove selection)
1147                if (rowCount > 0)
1148                {
1149                    // save anchor index which is lost with 'RowsUpdated' event
1150                    final int anchorInd = ((DefaultListSelectionModel) roiSelectionModel).getAnchorSelectionIndex();
1151
1152                    synchronized (roiTableModel)
1153                    {
1154                        try
1155                        {
1156                            roiTableModel.fireTableRowsUpdated(0, rowCount - 1);
1157                        }
1158                        catch (Exception e)
1159                        {
1160                            // Sorter don't like when we change data while it's sorting...
1161                        }
1162                    }
1163
1164                    // restore anchor index
1165                    if (anchorInd != -1)
1166                        ((DefaultListSelectionModel) roiSelectionModel).setAnchorSelectionIndex(anchorInd);
1167                }
1168            }
1169        });
1170
1171        refreshRoiNumbers();
1172    }
1173
1174    public void refreshTableSelection()
1175    {
1176        processor.submit(true, tableSelectionRefresher);
1177    }
1178
1179    protected void refreshTableSelectionInternal()
1180    {
1181        // don't eat too much time on selection refresh
1182        ThreadUtil.sleep(1);
1183
1184        final Set<ROI> newSelectedRois;
1185        final Sequence sequence = getSequence();
1186
1187        if (sequence != null)
1188            newSelectedRois = sequence.getSelectedROISet();
1189        else
1190            newSelectedRois = new HashSet<ROI>();
1191
1192        ThreadUtil.invokeNow(new Runnable()
1193        {
1194            @Override
1195            public void run()
1196            {
1197                modifySelection.acquireUninterruptibly();
1198                try
1199                {
1200                    // set selection
1201                    setSelectedRoisInternal(newSelectedRois);
1202                }
1203                finally
1204                {
1205                    modifySelection.release();
1206                }
1207            }
1208        });
1209
1210        refreshRoiNumbers();
1211    }
1212
1213    protected void refreshDescriptorList()
1214    {
1215        descriptorMap = ROIUtil.getROIDescriptors();
1216        refreshColumnInfoList();
1217    }
1218
1219    public void refreshColumnInfoList()
1220    {
1221        processor.submit(true, columnInfoListRefresher);
1222    }
1223
1224    protected void refreshColumnInfoListInternal()
1225    {
1226        // rebuild the column property list
1227        final List<ColumnInfo> newColumnInfos = new ArrayList<ColumnInfo>();
1228        final int numChannel = getChannelCount();
1229
1230        for (ROIDescriptor descriptor : descriptorMap.keySet())
1231        {
1232            for (int ch = 0; ch < (descriptor.separateChannel() ? numChannel : 1); ch++)
1233                newColumnInfos.add(new ColumnInfo(descriptor, ch, viewPreferences, false));
1234        }
1235
1236        // sort the list on order
1237        Collections.sort(newColumnInfos);
1238        // set new column info
1239        columnInfoList = newColumnInfos;
1240        // rebuild table columns
1241        ThreadUtil.invokeNow(new Runnable()
1242        {
1243            @Override
1244            public void run()
1245            {
1246                // regenerate column model
1247                roiTable.setColumnModel(new ROITableColumnModel());
1248            }
1249        });
1250    }
1251
1252    // protected void forceComputationForColumn(int column)
1253    // {
1254    // final int numRow = roiTableModel.getRowCount();
1255    //
1256    // try
1257    // {
1258    // // force computation of the column values if needed
1259    // for (int i = 0; i < numRow; i++)
1260    // roiTableModel.getValueAt(i, column);
1261    // }
1262    // catch (Exception e)
1263    // {
1264    // // ignore
1265    // System.out.println(e);
1266    // }
1267    // }
1268
1269    // protected boolean hasPendingComputation(ROIResults results)
1270    // {
1271    // return primaryDescriptorComputer.hasPendingComputation(results)
1272    // || basicDescriptorComputer.hasPendingComputation(results)
1273    // || advancedDescriptorComputer.hasPendingComputation(results);
1274    // }
1275
1276    protected void requestDescriptorComputation(ROIResults results)
1277    {
1278        primaryDescriptorComputer.requestDescriptorComputation(results);
1279        basicDescriptorComputer.requestDescriptorComputation(results);
1280        advancedDescriptorComputer.requestDescriptorComputation(results);
1281    }
1282
1283    protected void cancelDescriptorComputation(ROIResults results)
1284    {
1285        primaryDescriptorComputer.cancelDescriptorComputation(results);
1286        basicDescriptorComputer.cancelDescriptorComputation(results);
1287        advancedDescriptorComputer.cancelDescriptorComputation(results);
1288    }
1289
1290    protected void cancelDescriptorComputation(ROI roi)
1291    {
1292        primaryDescriptorComputer.cancelDescriptorComputation(roi);
1293        basicDescriptorComputer.cancelDescriptorComputation(roi);
1294        advancedDescriptorComputer.cancelDescriptorComputation(roi);
1295    }
1296
1297    protected void cancelAllDescriptorComputation()
1298    {
1299        primaryDescriptorComputer.cancelAllDescriptorComputation();
1300        basicDescriptorComputer.cancelAllDescriptorComputation();
1301        advancedDescriptorComputer.cancelAllDescriptorComputation();
1302    }
1303
1304    /**
1305     * @deprecated Use {@link #getCSVFormattedInfos()} instead.
1306     */
1307    @Deprecated
1308    public String getCSVFormattedInfosOfSelectedRois()
1309    {
1310        // Check to ensure we have selected only a contiguous block of cells
1311        final int numcols = roiTable.getColumnCount();
1312        final int numrows = roiTable.getSelectedRowCount();
1313
1314        // roiTable is empty --> returns empty string
1315        if (numrows == 0)
1316            return "";
1317
1318        final StringBuffer sbf = new StringBuffer();
1319        final int[] rowsselected = roiTable.getSelectedRows();
1320
1321        // column name
1322        for (int j = 1; j < numcols; j++)
1323        {
1324            sbf.append(roiTable.getModel().getColumnName(j));
1325            if (j < numcols - 1)
1326                sbf.append("\t");
1327        }
1328        sbf.append("\r\n");
1329
1330        // then content
1331        for (int i = 0; i < numrows; i++)
1332        {
1333            for (int j = 1; j < numcols; j++)
1334            {
1335                final Object value = roiTable.getModel().getValueAt(roiTable.convertRowIndexToModel(rowsselected[i]),
1336                        j);
1337
1338                // special case of double array
1339                if (value instanceof double[])
1340                {
1341                    final double[] darray = (double[]) value;
1342
1343                    for (int l = 0; l < darray.length; l++)
1344                    {
1345                        sbf.append(darray[l]);
1346                        if (l < darray.length - 1)
1347                            sbf.append(" ");
1348                    }
1349                }
1350                else
1351                    sbf.append(value);
1352
1353                if (j < numcols - 1)
1354                    sbf.append("\t");
1355            }
1356            sbf.append("\r\n");
1357        }
1358
1359        return sbf.toString();
1360    }
1361
1362    /**
1363     * Returns all ROI informations in CSV format (tab separated) immediately.
1364     */
1365    public String getCSVFormattedInfos()
1366    {
1367        final List<ColumnInfo> exportColumnInfos = new ArrayList<ColumnInfo>();
1368        final Sequence seq = getSequence();
1369        final int numChannel = getChannelCount();
1370
1371        // get export column informations
1372        for (ROIDescriptor descriptor : descriptorMap.keySet())
1373        {
1374            for (int ch = 0; ch < (descriptor.separateChannel() ? numChannel : 1); ch++)
1375                exportColumnInfos.add(new ColumnInfo(descriptor, ch, exportPreferences, true));
1376        }
1377
1378        // sort the list on order
1379        Collections.sort(exportColumnInfos);
1380
1381        final StringBuffer sbf = new StringBuffer();
1382
1383        // column title
1384        for (ColumnInfo columnInfo : exportColumnInfos)
1385        {
1386            if (columnInfo.visible)
1387            {
1388                sbf.append(columnInfo.name);
1389                sbf.append("\t");
1390            }
1391        }
1392        sbf.append("\r\n");
1393
1394        final List<ROI> rois = new ArrayList<ROI>(filteredRoiList);
1395
1396        // content
1397        for (ROI roi : rois)
1398        {
1399            final ROIResults results = createNewROIResults(roi);
1400            final Map<ColumnInfo, DescriptorResult> descriptorResults = results.descriptorResults;
1401
1402            // compute results
1403            for (ColumnInfo columnInfo : exportColumnInfos)
1404            {
1405                if (columnInfo.visible)
1406                {
1407                    // try to retrieve result for this column
1408                    final DescriptorResult result = descriptorResults.get(columnInfo);
1409
1410                    // not yet created/computed --> create it and compute it now
1411                    if (result == null)
1412                    {
1413                        descriptorResults.put(columnInfo, new DescriptorResult(columnInfo));
1414                        computeROIResults(results, seq, columnInfo);
1415                    }
1416                }
1417            }
1418
1419            // display results
1420            for (ColumnInfo columnInfo : exportColumnInfos)
1421            {
1422                if (columnInfo.visible)
1423                {
1424                    final DescriptorResult result = descriptorResults.get(columnInfo);
1425                    final String id = columnInfo.descriptor.getId();
1426                    final Object value;
1427
1428                    if (result != null)
1429                        value = results.formatValue(result.getValue(), id);
1430                    else
1431                        value = null;
1432
1433                    if (value != null)
1434                    {
1435                        // special case of icon --> use the ROI class name
1436                        if (StringUtil.equals(id, ROIIconDescriptor.ID))
1437                            sbf.append(roi.getSimpleClassName());
1438                        // special case of color --> use the color code
1439                        else if (StringUtil.equals(id, ROIColorDescriptor.ID))
1440                            sbf.append(String.format("%06X", Integer.valueOf(roi.getColor().getRGB() & 0xFFFFFF)));
1441                        else
1442                            sbf.append(value);
1443                    }
1444
1445                    sbf.append("\t");
1446                }
1447            }
1448            sbf.append("\r\n");
1449        }
1450
1451        return sbf.toString();
1452    }
1453
1454    public void showSettingPanel()
1455    {
1456        // create and display the setting frame
1457        new RoiSettingFrame(viewPreferences, exportPreferences, new Runnable()
1458        {
1459            @Override
1460            public void run()
1461            {
1462                // refresh table columns
1463                refreshColumnInfoListInternal();
1464            }
1465        });
1466    }
1467
1468    @Override
1469    public void textChanged(IcyTextField source, boolean validate)
1470    {
1471        if (source == nameFilter)
1472            refreshFilteredRois();
1473    }
1474
1475    // called when selection changed in the ROI table
1476    @Override
1477    public void valueChanged(ListSelectionEvent e)
1478    {
1479        // currently changing the selection ? --> exit
1480        if (e.getValueIsAdjusting())
1481            return;
1482        // currently changing the selection ? --> exit
1483        if (roiSelectionModel.getValueIsAdjusting())
1484            return;
1485
1486        if (modifySelection.tryAcquire())
1487        {
1488            // semaphore acquired here
1489            try
1490            {
1491                final List<ROI> selectedRois = getSelectedRois();
1492                final Sequence sequence = getSequence();
1493
1494                // update selected ROI in sequence
1495                if (sequence != null)
1496                    sequence.setSelectedROIs(selectedRois);
1497            }
1498            finally
1499            {
1500                modifySelection.release();
1501            }
1502        }
1503
1504        refreshRoiNumbers();
1505    }
1506
1507    // called when a ROI has been double clicked in the ROI table
1508    protected void roiTableDoubleClicked()
1509    {
1510        final List<ROI> selectedRois = getSelectedRois();
1511
1512        if (selectedRois.size() > 0)
1513        {
1514            final ROI selected = selectedRois.get(0);
1515            // get active viewer
1516            final Viewer v = Icy.getMainInterface().getActiveViewer();
1517
1518            if ((v != null) && (selected != null))
1519            {
1520                // get canvas
1521                final IcyCanvas canvas = v.getCanvas();
1522
1523                if (canvas instanceof IcyCanvas2D)
1524                {
1525                    // center view on selected ROI
1526                    ((IcyCanvas2D) canvas).centerOn(selected.getBounds5D().toRectangle2D().getBounds());
1527                }
1528                else if (canvas instanceof IcyCanvas3D)
1529                {
1530                    // center view on selected ROI
1531                    ((IcyCanvas3D) canvas).centerOn(selected.getBounds5D().toRectangle3D().toInteger());
1532                }
1533
1534                final Rectangle5D bnd = selected.getBounds5D();
1535                final int t = (int) (bnd.isInfiniteT() ? -1 : bnd.getCenterT());
1536                final int z = (int) (bnd.isInfiniteZ() ? -1 : bnd.getCenterZ());
1537
1538                // change position if needed
1539                if (t != -1)
1540                    v.setPositionT(t);
1541                if (z != -1)
1542                    v.setPositionZ(z);
1543            }
1544        }
1545    }
1546
1547    @Override
1548    public void sequenceActivated(Sequence value)
1549    {
1550        // refresh table columns
1551        refreshColumnInfoList();
1552        // refresh ROI list
1553        refreshRois();
1554    }
1555
1556    @Override
1557    public void sequenceDeactivated(Sequence sequence)
1558    {
1559        // nothing here
1560    }
1561
1562    @Override
1563    public void activeSequenceChanged(SequenceEvent event)
1564    {
1565        // we are modifying externally
1566        // if (modifySelection.availablePermits() == 0)
1567        // return;
1568
1569        final SequenceEventSourceType sourceType = event.getSourceType();
1570
1571        switch (sourceType)
1572        {
1573            case SEQUENCE_ROI:
1574                switch (event.getType())
1575                {
1576                    case ADDED:
1577                    case REMOVED:
1578                        refreshRois();
1579                        break;
1580
1581                    case CHANGED:
1582                        // already handled by ROIResults directly
1583                        break;
1584                }
1585                break;
1586
1587            case SEQUENCE_META:
1588                // refresh column name (unit can change when pixel size changed)
1589                for (ColumnInfo col : columnInfoList)
1590                    col.refreshName();
1591
1592                // refresh column model
1593                final TableColumnModel model = roiTable.getColumnModel();
1594                if (model instanceof ROITableColumnModel)
1595                    ((ROITableColumnModel) model).updateHeaders();
1596
1597                // don't use break, we also need to send the event to descriptors
1598
1599            case SEQUENCE_DATA:
1600                final ROIResults[] allRoiResults;
1601
1602                // get all ROI results
1603                synchronized (roiResultsMap)
1604                {
1605                    allRoiResults = roiResultsMap.values().toArray(new ROIResults[roiResultsMap.size()]);
1606                }
1607
1608                // notify ROI results that sequence has changed
1609                for (ROIResults roiResults : allRoiResults)
1610                    roiResults.sequenceChanged(event);
1611
1612                // refresh table data
1613                refreshTableData();
1614                break;
1615
1616            case SEQUENCE_TYPE:
1617                // number of channel can have changed
1618                refreshColumnInfoList();
1619                break;
1620        }
1621    }
1622
1623    @Override
1624    public void pluginLoaderChanged(PluginLoaderEvent e)
1625    {
1626        refreshDescriptorList();
1627    }
1628
1629    protected class ROITableModel extends AbstractTableModel
1630    {
1631        /**
1632         * 
1633         */
1634        private static final long serialVersionUID = -6537163170625368503L;
1635
1636        public ROITableModel()
1637        {
1638            super();
1639        }
1640
1641        @Override
1642        public int getColumnCount()
1643        {
1644            return columnInfoList.size();
1645        }
1646
1647        @Override
1648        public String getColumnName(int column)
1649        {
1650            final ColumnInfo ci = getColumnInfo(column);
1651
1652            if ((ci != null) && (ci.showName))
1653                return ci.name;
1654
1655            return "";
1656        }
1657
1658        @Override
1659        public Class<?> getColumnClass(int column)
1660        {
1661            final ColumnInfo ci = getColumnInfo(column);
1662
1663            if (ci != null)
1664                return ci.descriptor.getType();
1665
1666            return String.class;
1667        }
1668
1669        @Override
1670        public int getRowCount()
1671        {
1672            return filteredRoiResultsList.size();
1673        }
1674
1675        @Override
1676        public Object getValueAt(int row, int column)
1677        {
1678            final ROIResults roiResults = getRoiResults(row);
1679
1680            if (roiResults != null)
1681                return roiResults.getValueAt(column);
1682
1683            return null;
1684        }
1685
1686        @Override
1687        public void setValueAt(Object value, int row, int column)
1688        {
1689            final ROIResults roiResults = getRoiResults(row);
1690
1691            if (roiResults != null)
1692                roiResults.setValueAt(value, column);
1693        }
1694
1695        @Override
1696        public boolean isCellEditable(int row, int column)
1697        {
1698            final ROIResults roiResults = getRoiResults(row);
1699
1700            if (roiResults != null)
1701                return roiResults.isEditable(column);
1702
1703            return false;
1704        }
1705    }
1706
1707    protected class ROIResults implements ROIListener
1708    {
1709        public final Map<ColumnInfo, DescriptorResult> descriptorResults;
1710        public final ROI roi;
1711        private final Map<Integer, WeakReference<ROI>> channelRois;
1712
1713        protected ROIResults(ROI roi)
1714        {
1715            super();
1716
1717            this.roi = roi;
1718            descriptorResults = new HashMap<ColumnInfo, DescriptorResult>();
1719            channelRois = new HashMap<Integer, WeakReference<ROI>>();
1720
1721            // listen for ROI change event
1722            roi.addListener(this);
1723        }
1724
1725        // boolean areResultsUpToDate()
1726        // {
1727        // for (DescriptorResult result : descriptorResults.values())
1728        // if (result.isOutdated())
1729        // return false;
1730        //
1731        // return true;
1732        // }
1733
1734        private void clearChannelRois()
1735        {
1736            synchronized (channelRois)
1737            {
1738                channelRois.clear();
1739            }
1740        }
1741
1742        public ROI getRoiForChannel(int channel)
1743        {
1744            final Integer key = Integer.valueOf(channel);
1745            WeakReference<ROI> reference;
1746            ROI result;
1747
1748            synchronized (channelRois)
1749            {
1750                reference = channelRois.get(key);
1751            }
1752
1753            if (reference != null)
1754                result = reference.get();
1755            else
1756                result = null;
1757
1758            // channel ROI does not exist ?
1759            if (result == null)
1760            {
1761                // create it
1762                result = roi.getSubROI(-1, -1, channel);
1763
1764                // failed ? try again
1765                if (result == null)
1766                    result = roi.getSubROI(-1, -1, channel);
1767
1768                if (result != null)
1769                {
1770                    // and put it in map
1771                    synchronized (channelRois)
1772                    {
1773                        // we use WeakReference to not waste memory
1774                        channelRois.put(key, new WeakReference<ROI>(result));
1775                    }
1776                }
1777            }
1778
1779            return result;
1780        }
1781
1782        public boolean isEditable(int column)
1783        {
1784            final ColumnInfo ci = getColumnInfo(column);
1785
1786            if (ci != null)
1787            {
1788                final ROIDescriptor descriptor = ci.descriptor;
1789                final String id = descriptor.getId();
1790
1791                // only name and color descriptor are editable (a bit hacky)
1792                return id.equals(ROINameDescriptor.ID) || id.equals(ROIColorDescriptor.ID);
1793            }
1794
1795            return false;
1796        }
1797
1798        public Object formatValue(Object value, String id)
1799        {
1800            Object result = value;
1801
1802            // format result if needed
1803            if (result instanceof Number)
1804            {
1805                final double doubleValue = ((Number) result).doubleValue();
1806
1807                // replace 'infinity' by infinite symbol
1808                if (doubleValue == Double.POSITIVE_INFINITY)
1809                    result = MathUtil.INFINITE_STRING;
1810                else if (doubleValue == Double.NEGATIVE_INFINITY)
1811                {
1812                    // position descriptor ? negative infinite means 'ALL' here
1813                    if (id.equals(ROIPositionXDescriptor.ID) || id.equals(ROIPositionYDescriptor.ID)
1814                            || id.equals(ROIPositionZDescriptor.ID) || id.equals(ROIPositionTDescriptor.ID)
1815                            || id.equals(ROIPositionCDescriptor.ID) || id.equals(ROIMassCenterXDescriptor.ID)
1816                            || id.equals(ROIMassCenterYDescriptor.ID) || id.equals(ROIMassCenterZDescriptor.ID)
1817                            || id.equals(ROIMassCenterTDescriptor.ID) || id.equals(ROIMassCenterCDescriptor.ID))
1818                        result = "ALL";
1819                    else
1820                        result = "-" + MathUtil.INFINITE_STRING;
1821                }
1822                else if (doubleValue == -1d)
1823                {
1824                    // position descriptor ? -1 means 'ALL' here
1825                    if (id.equals(ROIPositionXDescriptor.ID) || id.equals(ROIPositionYDescriptor.ID)
1826                            || id.equals(ROIPositionZDescriptor.ID) || id.equals(ROIPositionTDescriptor.ID)
1827                            || id.equals(ROIPositionCDescriptor.ID) || id.equals(ROIMassCenterXDescriptor.ID)
1828                            || id.equals(ROIMassCenterYDescriptor.ID) || id.equals(ROIMassCenterZDescriptor.ID)
1829                            || id.equals(ROIMassCenterTDescriptor.ID) || id.equals(ROIMassCenterCDescriptor.ID))
1830                        result = "ALL";
1831                }
1832                else
1833                {
1834                    // value not too large ?
1835                    if (Math.abs(doubleValue) < 10000000)
1836                    {
1837                        // simple integer ? -> show it as integer
1838                        if (doubleValue == (int) doubleValue)
1839                            result = Integer.valueOf((int) doubleValue);
1840                        // small integer value ?
1841                        else if (Math.abs(doubleValue) < 100)
1842                            result = Double.valueOf(MathUtil.roundSignificant(doubleValue, 5));
1843                        // medium integer value ?
1844                        else if (Math.abs(doubleValue) < 10000)
1845                            result = Double.valueOf(MathUtil.round(doubleValue, 2));
1846                        // medium large integer value ?
1847                        else if (Math.abs(doubleValue) < 1000000)
1848                            result = Double.valueOf(MathUtil.round(doubleValue, 1));
1849                        else
1850                            // large integer value ?
1851                            result = Integer.valueOf((int) Math.round(doubleValue));
1852                    }
1853                    else
1854                        // format double value
1855                        result = Double.valueOf(MathUtil.roundSignificant(doubleValue, 5));
1856                }
1857            }
1858
1859            return result;
1860        }
1861
1862        /**
1863         * Retrieve the DescriptorResult for the specified column
1864         */
1865        public DescriptorResult getDescriptorResult(ColumnInfo column)
1866        {
1867            // get result for this descriptor
1868            DescriptorResult result;
1869
1870            synchronized (descriptorResults)
1871            {
1872                result = descriptorResults.get(column);
1873
1874                // no result --> create it and request computation
1875                if (result == null)
1876                {
1877                    // create descriptor result
1878                    result = new DescriptorResult(column);
1879                    // and put it in results map
1880                    descriptorResults.put(column, result);
1881                }
1882            }
1883
1884            return result;
1885        }
1886
1887        /**
1888         * Retrieve the value for the specified descriptor
1889         */
1890        public Object getValue(ColumnInfo column)
1891        {
1892            // get result for this descriptor
1893            final DescriptorResult result = getDescriptorResult(column);
1894
1895            // out dated result ? --> request for descriptor computation
1896            if (result.isOutdated())
1897                requestDescriptorComputation(this);
1898
1899            return formatValue(result.getValue(), column.descriptor.getId());
1900        }
1901
1902        public Object getValueAt(int column)
1903        {
1904            final ColumnInfo ci = getColumnInfo(column);
1905
1906            if (ci != null)
1907                return getValue(ci);
1908
1909            return null;
1910        }
1911
1912        public void setValueAt(Object aValue, int column)
1913        {
1914            final ColumnInfo ci = getColumnInfo(column);
1915
1916            if (ci != null)
1917            {
1918                final ROIDescriptor descriptor = ci.descriptor;
1919                final String id = descriptor.getId();
1920
1921                // only name descriptor is editable (a bit hacky)
1922                if (id.equals(ROINameDescriptor.ID))
1923                    roi.setName((String) aValue);
1924                else if (id.equals(ROIColorDescriptor.ID))
1925                    roi.setColor((Color) aValue);
1926            }
1927        }
1928
1929        @Override
1930        public void roiChanged(ROIEvent event)
1931        {
1932            switch (event.getType())
1933            {
1934                case ROI_CHANGED:
1935                case PROPERTY_CHANGED:
1936                    final Object[] entries;
1937
1938                    synchronized (descriptorResults)
1939                    {
1940                        entries = descriptorResults.entrySet().toArray();
1941                    }
1942
1943                    for (Object entryObj : entries)
1944                    {
1945                        final Entry<ColumnInfo, DescriptorResult> entry = (Entry<ColumnInfo, DescriptorResult>) entryObj;
1946                        final ColumnInfo key = entry.getKey();
1947                        final ROIDescriptor descriptor = key.descriptor;
1948
1949                        // need to recompute this descriptor ?
1950                        if (descriptor.needRecompute(event))
1951                        {
1952                            final DescriptorResult result = entry.getValue();
1953
1954                            // mark as outdated
1955                            if (result != null)
1956                                result.setOutdated(true);
1957                        }
1958                    }
1959
1960                    // need to recompute channel rois
1961                    if (event.getType() == ROIEventType.ROI_CHANGED)
1962                        clearChannelRois();
1963
1964                    // and refresh table data
1965                    refreshTableData();
1966                    break;
1967
1968                case SELECTION_CHANGED:
1969                    // not modifying selection from panel ?
1970                    if (modifySelection.availablePermits() > 0)
1971                        // update ROI selection
1972                        refreshTableSelection();
1973                    break;
1974            }
1975        }
1976
1977        /**
1978         * Called when the sequence changed, in which case we need to invalidate results.
1979         * 
1980         * @param event
1981         *        Sequence change event
1982         */
1983        public void sequenceChanged(SequenceEvent event)
1984        {
1985            final Object[] entries;
1986
1987            synchronized (descriptorResults)
1988            {
1989                entries = descriptorResults.entrySet().toArray();
1990            }
1991
1992            for (Object entryObj : entries)
1993            {
1994                final Entry<ColumnInfo, DescriptorResult> entry = (Entry<ColumnInfo, DescriptorResult>) entryObj;
1995                final ColumnInfo key = entry.getKey();
1996                final ROIDescriptor descriptor = key.descriptor;
1997
1998                // need to recompute this descriptor ?
1999                if (descriptor.needRecompute(event))
2000                {
2001                    final DescriptorResult result = entry.getValue();
2002
2003                    // mark as outdated
2004                    if (result != null)
2005                        result.setOutdated(true);
2006                }
2007            }
2008        }
2009    }
2010
2011    protected class DescriptorComputer extends Thread
2012    {
2013        protected final LinkedHashSet<ROIResults> resultsToCompute;
2014        protected final DescriptorType type;
2015
2016        public DescriptorComputer(DescriptorType type)
2017        {
2018            super("ROI " + type.toString() + " descriptor calculator");
2019
2020            resultsToCompute = new LinkedHashSet<AbstractRoisPanel.ROIResults>(256);
2021            this.type = type;
2022
2023            setPriority(Thread.MIN_PRIORITY);
2024        }
2025
2026        public boolean hasPendingComputation()
2027        {
2028            return resultsToCompute.size() > 0;
2029        }
2030
2031        public boolean hasPendingComputation(ROIResults results)
2032        {
2033            synchronized (resultsToCompute)
2034            {
2035                return resultsToCompute.contains(results);
2036            }
2037        }
2038
2039        public void requestDescriptorComputation(ROIResults results)
2040        {
2041            synchronized (resultsToCompute)
2042            {
2043                resultsToCompute.add(results);
2044                resultsToCompute.notifyAll();
2045            }
2046        }
2047
2048        public void cancelDescriptorComputation(ROIResults roiResults)
2049        {
2050            synchronized (resultsToCompute)
2051            {
2052                resultsToCompute.remove(roiResults);
2053                resultsToCompute.notifyAll();
2054            }
2055        }
2056
2057        public void cancelDescriptorComputation(ROI roi)
2058        {
2059            synchronized (resultsToCompute)
2060            {
2061                final Iterator<ROIResults> it = resultsToCompute.iterator();
2062
2063                while (it.hasNext())
2064                {
2065                    final ROIResults roiResults = it.next();
2066
2067                    // remove all results for this ROI
2068                    if (roiResults.roi == roi)
2069                        it.remove();
2070                }
2071
2072                resultsToCompute.notifyAll();
2073            }
2074        }
2075
2076        public void cancelAllDescriptorComputation()
2077        {
2078            synchronized (resultsToCompute)
2079            {
2080                resultsToCompute.clear();
2081                resultsToCompute.notifyAll();
2082            }
2083        }
2084
2085        @Override
2086        public void run()
2087        {
2088            while (!Thread.interrupted())
2089            {
2090                final ROIResults[] roiResultsList;
2091
2092                synchronized (resultsToCompute)
2093                {
2094                    try
2095                    {
2096                        while (resultsToCompute.isEmpty())
2097                            resultsToCompute.wait();
2098                    }
2099                    catch (InterruptedException e)
2100                    {
2101                        // ignore and just interrupt now
2102                        Thread.currentThread().interrupt();
2103                    }
2104
2105                    // get results to compute
2106                    roiResultsList = resultsToCompute.toArray(new ROIResults[resultsToCompute.size()]);
2107                    // and remove them
2108                    resultsToCompute.clear();
2109                }
2110
2111                final Sequence seq = getSequence();
2112
2113                if (seq != null)
2114                {
2115                    // start with primaries descriptors
2116                    for (ROIResults roiResults : roiResultsList)
2117                    {
2118                        // active sequence changed ? --> quickly discard other calculations
2119                        if (seq != getSequence())
2120                            break;
2121
2122                        computeROIResults(roiResults, seq);
2123                    }
2124                }
2125            }
2126        }
2127
2128        protected void computeROIResults(ROIResults roiResults, Sequence seq)
2129        {
2130            final Map<ColumnInfo, DescriptorResult> results = roiResults.descriptorResults;
2131            final ColumnInfo[] columnInfos;
2132
2133            synchronized (results)
2134            {
2135                columnInfos = results.keySet().toArray(new ColumnInfo[results.size()]);
2136            }
2137
2138            boolean needUpdate = false;
2139            for (ColumnInfo columnInfo : columnInfos)
2140            {
2141                // only compute a specific kind of descriptor
2142                if (columnInfo.getDescriptorType() == type)
2143                    needUpdate |= AbstractRoisPanel.this.computeROIResults(roiResults, seq, columnInfo);
2144            }
2145
2146            // need to refresh data
2147            if (needUpdate)
2148                refreshTableData();
2149        }
2150    }
2151
2152    protected class DescriptorResult
2153    {
2154        private Object value;
2155        private boolean outdated;
2156
2157        public DescriptorResult(ColumnInfo column)
2158        {
2159            super();
2160
2161            value = null;
2162
2163            // by default we consider it as out dated
2164            outdated = true;
2165        }
2166
2167        public Object getValue()
2168        {
2169            return value;
2170        }
2171
2172        public void setValue(Object value)
2173        {
2174            this.value = value;
2175        }
2176
2177        public boolean isOutdated()
2178        {
2179            return outdated;
2180        }
2181
2182        public void setOutdated(boolean value)
2183        {
2184            outdated = value;
2185        }
2186    }
2187
2188    public static enum DescriptorType
2189    {
2190        PRIMARY, BASIC, EXTERNAL
2191    };
2192
2193    public static class BaseColumnInfo implements Comparable<BaseColumnInfo>
2194    {
2195        public final ROIDescriptor descriptor;
2196        public int minSize;
2197        public int maxSize;
2198        public int defaultSize;
2199        public int order;
2200        public boolean visible;
2201
2202        public BaseColumnInfo(ROIDescriptor descriptor, XMLPreferences preferences, boolean export)
2203        {
2204            super();
2205
2206            this.descriptor = descriptor;
2207
2208            load(preferences, export);
2209        }
2210
2211        public boolean load(XMLPreferences preferences, boolean export)
2212        {
2213            final XMLPreferences p = preferences.node(descriptor.getId());
2214
2215            if (p != null)
2216            {
2217                minSize = p.getInt(ID_PROPERTY_MINSIZE, getDefaultMinSize());
2218                maxSize = p.getInt(ID_PROPERTY_MAXSIZE, getDefaultMaxSize());
2219                defaultSize = p.getInt(ID_PROPERTY_DEFAULTSIZE, getDefaultDefaultSize());
2220                order = p.getInt(ID_PROPERTY_ORDER, getDefaultOrder());
2221                visible = p.getBoolean(ID_PROPERTY_VISIBLE, getDefaultVisible(export));
2222
2223                return true;
2224            }
2225
2226            return false;
2227        }
2228
2229        public boolean save(XMLPreferences preferences)
2230        {
2231            final XMLPreferences p = preferences.node(descriptor.getId());
2232
2233            if (p != null)
2234            {
2235                // p.putInt(ID_PROPERTY_MINSIZE, minSize);
2236                // p.putInt(ID_PROPERTY_MAXSIZE, maxSize);
2237                // p.putInt(ID_PROPERTY_DEFAULTSIZE, defaultSize);
2238                p.putInt(ID_PROPERTY_ORDER, order);
2239                p.putBoolean(ID_PROPERTY_VISIBLE, visible);
2240
2241                return true;
2242            }
2243
2244            return false;
2245        }
2246
2247        protected boolean getDefaultVisible(boolean export)
2248        {
2249            if (descriptor == null)
2250                return false;
2251
2252            final String id = descriptor.getId();
2253
2254            if (export)
2255            {
2256                if (StringUtil.equals(id, ROIOpacityDescriptor.ID))
2257                    return false;
2258
2259                final Class<?> type = descriptor.getType();
2260                return ClassUtil.isSubClass(type, String.class) || ClassUtil.isSubClass(type, Number.class);
2261            }
2262
2263            if (StringUtil.equals(id, ROIIconDescriptor.ID))
2264                return true;
2265            if (StringUtil.equals(id, ROINameDescriptor.ID))
2266                return true;
2267
2268            if (StringUtil.equals(id, ROIContourDescriptor.ID))
2269                return true;
2270            if (StringUtil.equals(id, ROIInteriorDescriptor.ID))
2271                return true;
2272
2273            return false;
2274        }
2275
2276        protected int getDefaultOrder()
2277        {
2278            if (descriptor == null)
2279                return Integer.MAX_VALUE;
2280
2281            final String id = descriptor.getId();
2282            int order = -1;
2283
2284            order++;
2285            if (StringUtil.equals(id, ROIIconDescriptor.ID))
2286                return order;
2287            order++;
2288            if (StringUtil.equals(id, ROIColorDescriptor.ID))
2289                return order;
2290            order++;
2291            if (StringUtil.equals(id, ROIGroupIdDescriptor.ID))
2292                return order;
2293            order++;
2294            if (StringUtil.equals(id, ROINameDescriptor.ID))
2295                return order;
2296
2297            order++;
2298            if (StringUtil.equals(id, ROIPositionXDescriptor.ID))
2299                return order;
2300            order++;
2301            if (StringUtil.equals(id, ROIPositionYDescriptor.ID))
2302                return order;
2303            order++;
2304            if (StringUtil.equals(id, ROIPositionZDescriptor.ID))
2305                return order;
2306            order++;
2307            if (StringUtil.equals(id, ROIPositionTDescriptor.ID))
2308                return order;
2309            order++;
2310            if (StringUtil.equals(id, ROIPositionCDescriptor.ID))
2311                return order;
2312
2313            order++;
2314            if (StringUtil.equals(id, ROISizeXDescriptor.ID))
2315                return order;
2316            order++;
2317            if (StringUtil.equals(id, ROISizeYDescriptor.ID))
2318                return order;
2319            order++;
2320            if (StringUtil.equals(id, ROISizeZDescriptor.ID))
2321                return order;
2322            order++;
2323            if (StringUtil.equals(id, ROISizeTDescriptor.ID))
2324                return order;
2325            order++;
2326            if (StringUtil.equals(id, ROISizeCDescriptor.ID))
2327                return order;
2328
2329            order++;
2330            if (StringUtil.equals(id, ROIMassCenterXDescriptor.ID))
2331                return order;
2332            order++;
2333            if (StringUtil.equals(id, ROIMassCenterYDescriptor.ID))
2334                return order;
2335            order++;
2336            if (StringUtil.equals(id, ROIMassCenterZDescriptor.ID))
2337                return order;
2338            order++;
2339            if (StringUtil.equals(id, ROIMassCenterTDescriptor.ID))
2340                return order;
2341            order++;
2342            if (StringUtil.equals(id, ROIMassCenterCDescriptor.ID))
2343                return order;
2344
2345            order++;
2346            if (StringUtil.equals(id, ROIContourDescriptor.ID))
2347                return order;
2348            order++;
2349            if (StringUtil.equals(id, ROIInteriorDescriptor.ID))
2350                return order;
2351
2352            order++;
2353            if (StringUtil.equals(id, ROIPerimeterDescriptor.ID))
2354                return order;
2355            order++;
2356            if (StringUtil.equals(id, ROIAreaDescriptor.ID))
2357                return order;
2358            order++;
2359            if (StringUtil.equals(id, ROISurfaceAreaDescriptor.ID))
2360                return order;
2361            order++;
2362            if (StringUtil.equals(id, ROIVolumeDescriptor.ID))
2363                return order;
2364
2365            order++;
2366            if (StringUtil.equals(id, ROIMinIntensityDescriptor.ID))
2367                return order;
2368            order++;
2369            if (StringUtil.equals(id, ROIMeanIntensityDescriptor.ID))
2370                return order;
2371            order++;
2372            if (StringUtil.equals(id, ROIMaxIntensityDescriptor.ID))
2373                return order;
2374            order++;
2375            if (StringUtil.equals(id, ROISumIntensityDescriptor.ID))
2376                return order;
2377
2378            return Integer.MAX_VALUE;
2379        }
2380
2381        protected int getDefaultMinSize()
2382        {
2383            if (descriptor == null)
2384                return Integer.MAX_VALUE;
2385
2386            final String id = descriptor.getId();
2387
2388            if (StringUtil.equals(id, ROIIconDescriptor.ID))
2389                return 22;
2390            if (StringUtil.equals(id, ROIColorDescriptor.ID))
2391                return 18;
2392            if (StringUtil.equals(id, ROIGroupIdDescriptor.ID))
2393                return 18;
2394            if (StringUtil.equals(id, ROINameDescriptor.ID))
2395                return 60;
2396
2397            final Class<?> type = descriptor.getType();
2398
2399            if (type == Integer.class)
2400                return 30;
2401            if (type == Float.class)
2402                return 40;
2403            if (type == Double.class)
2404                return 40;
2405            if (type == String.class)
2406                return 50;
2407
2408            return 40;
2409        }
2410
2411        protected int getDefaultMaxSize()
2412        {
2413            if (descriptor == null)
2414                return Integer.MAX_VALUE;
2415
2416            final String id = descriptor.getId();
2417
2418            if (StringUtil.equals(id, ROIIconDescriptor.ID))
2419                return 22;
2420            if (StringUtil.equals(id, ROIColorDescriptor.ID))
2421                return 18;
2422            if (StringUtil.equals(id, ROIGroupIdDescriptor.ID))
2423                return 18;
2424
2425            return Integer.MAX_VALUE;
2426        }
2427
2428        protected int getDefaultDefaultSize()
2429        {
2430            final int maxSize = getDefaultMaxSize();
2431            final int minSize = getDefaultMinSize();
2432
2433            if (maxSize == Integer.MAX_VALUE)
2434                return minSize * 2;
2435
2436            return (minSize + maxSize) / 2;
2437        }
2438
2439        /**
2440         * Used to know if this is a primary (name, color...) ROI descriptor.
2441         * 
2442         * @see #isBasicDescriptor()
2443         * @see #isExtendedDescriptor()
2444         * @see #getDescriptorType()
2445         */
2446        protected boolean isPrimaryDescriptor()
2447        {
2448            if (descriptor == null)
2449                return false;
2450
2451            final String id = descriptor.getId();
2452
2453            return (StringUtil.equals(id, ROIIconDescriptor.ID)) || (StringUtil.equals(id, ROIColorDescriptor.ID))
2454                    || (StringUtil.equals(id, ROINameDescriptor.ID)) || (StringUtil.equals(id, ROIGroupIdDescriptor.ID))
2455                    || (StringUtil.equals(id, ROIPositionXDescriptor.ID))
2456                    || (StringUtil.equals(id, ROIPositionYDescriptor.ID))
2457                    || (StringUtil.equals(id, ROIPositionZDescriptor.ID))
2458                    || (StringUtil.equals(id, ROIPositionTDescriptor.ID))
2459                    || (StringUtil.equals(id, ROIPositionCDescriptor.ID))
2460                    || (StringUtil.equals(id, ROISizeXDescriptor.ID)) || (StringUtil.equals(id, ROISizeYDescriptor.ID))
2461                    || (StringUtil.equals(id, ROISizeZDescriptor.ID)) || (StringUtil.equals(id, ROISizeTDescriptor.ID))
2462                    || (StringUtil.equals(id, ROISizeCDescriptor.ID));
2463        }
2464
2465        /**
2466         * Used to know if this is a primary or basic (interior, contour, intensities..) ROI descriptor.
2467         * 
2468         * @see #isPrimaryDescriptor()
2469         * @see #isExtendedDescriptor()
2470         * @see #getDescriptorType()
2471         */
2472        protected boolean isBasicDescriptor()
2473        {
2474            if (descriptor == null)
2475                return false;
2476
2477            final String id = descriptor.getId();
2478
2479            return isPrimaryDescriptor() || (StringUtil.equals(id, ROIMassCenterXDescriptor.ID))
2480                    || (StringUtil.equals(id, ROIMassCenterYDescriptor.ID))
2481                    || (StringUtil.equals(id, ROIMassCenterZDescriptor.ID))
2482                    || (StringUtil.equals(id, ROIMassCenterTDescriptor.ID))
2483                    || (StringUtil.equals(id, ROIMassCenterCDescriptor.ID))
2484                    || (StringUtil.equals(id, ROIContourDescriptor.ID))
2485                    || (StringUtil.equals(id, ROIInteriorDescriptor.ID))
2486                    || (StringUtil.equals(id, ROIPerimeterDescriptor.ID))
2487                    || (StringUtil.equals(id, ROIAreaDescriptor.ID))
2488                    || (StringUtil.equals(id, ROISurfaceAreaDescriptor.ID))
2489                    || (StringUtil.equals(id, ROIVolumeDescriptor.ID))
2490                    || (StringUtil.equals(id, ROIMinIntensityDescriptor.ID))
2491                    || (StringUtil.equals(id, ROIMeanIntensityDescriptor.ID))
2492                    || (StringUtil.equals(id, ROIMaxIntensityDescriptor.ID));
2493        }
2494
2495        /**
2496         * Used to know if this is a extended (added by an external plugin) ROI descriptor
2497         * 
2498         * @see #isPrimaryDescriptor()
2499         * @see #isBasicDescriptor()
2500         * @see #getDescriptorType()
2501         */
2502        protected boolean isExtendedDescriptor()
2503        {
2504            if (descriptor == null)
2505                return false;
2506
2507            return !isBasicDescriptor();
2508        }
2509
2510        /**
2511         * Returns the kind of ROI descriptor
2512         */
2513        public DescriptorType getDescriptorType()
2514        {
2515            if (descriptor == null)
2516                return null;
2517
2518            if (isPrimaryDescriptor())
2519                return DescriptorType.PRIMARY;
2520            if (isBasicDescriptor())
2521                return DescriptorType.BASIC;
2522
2523            return DescriptorType.EXTERNAL;
2524        }
2525
2526        @Override
2527        public int compareTo(BaseColumnInfo obj)
2528        {
2529            return Integer.valueOf(order).compareTo(Integer.valueOf(obj.order));
2530        }
2531
2532        @Override
2533        public int hashCode()
2534        {
2535            return descriptor.hashCode();
2536        }
2537
2538        @Override
2539        public boolean equals(Object obj)
2540        {
2541            if (obj instanceof BaseColumnInfo)
2542                // equality on descriptor
2543                return ((BaseColumnInfo) obj).descriptor.equals(descriptor);
2544
2545            return super.equals(obj);
2546        }
2547    }
2548
2549    protected class ColumnInfo extends BaseColumnInfo
2550    {
2551        boolean showName;
2552        String name;
2553        final int channel;
2554
2555        public ColumnInfo(ROIDescriptor descriptor, int channel, XMLPreferences prefs, boolean export)
2556        {
2557            super(descriptor, prefs, export);
2558
2559            this.channel = channel;
2560            refreshName();
2561        }
2562
2563        protected String getSuffix()
2564        {
2565            String result = "";
2566
2567            final String unit = descriptor.getUnit(getSequence());
2568
2569            if (!StringUtil.isEmpty(unit))
2570                result += " (" + unit + ")";
2571
2572            // separate channel
2573            if (descriptor.separateChannel())
2574                result += getChannelNameSuffix(channel);
2575
2576            return result;
2577        }
2578
2579        protected void refreshName()
2580        {
2581            name = descriptor.getName() + getSuffix();
2582
2583            final String id = descriptor.getId();
2584
2585            // we don't want to display name for these descriptors
2586            if (StringUtil.equals(id, ROIIconDescriptor.ID) || StringUtil.equals(id, ROIColorDescriptor.ID)
2587                    || StringUtil.equals(id, ROIGroupIdDescriptor.ID))
2588                showName = false;
2589            else
2590                showName = true;
2591        }
2592
2593        @Override
2594        public int hashCode()
2595        {
2596            return descriptor.hashCode() ^ channel;
2597        }
2598
2599        @Override
2600        public boolean equals(Object obj)
2601        {
2602            if (obj instanceof ColumnInfo)
2603            {
2604                final ColumnInfo ci = (ColumnInfo) obj;
2605
2606                // equality on descriptor and channel number
2607                return (ci.descriptor.equals(descriptor) && (ci.channel == ci.channel));
2608            }
2609
2610            return super.equals(obj);
2611        }
2612    }
2613
2614    protected class ROITableColumnModel extends DefaultTableColumnModelExt
2615    {
2616        /**
2617         * 
2618         */
2619        private static final long serialVersionUID = -8024047283485991234L;
2620
2621        public ROITableColumnModel()
2622        {
2623            super();
2624
2625            final List<ColumnInfo> columnInfos = columnInfoList;
2626
2627            // column info are sorted on their order
2628            int index = 0;
2629            for (ColumnInfo ci : columnInfos)
2630            {
2631                final ROIDescriptor descriptor = ci.descriptor;
2632                final TableColumnExt column = new TableColumnExt(index++);
2633
2634                column.setIdentifier(descriptor.getId());
2635                column.setMinWidth(ci.minSize);
2636                column.setPreferredWidth(ci.defaultSize);
2637                if (ci.maxSize != Integer.MAX_VALUE)
2638                    column.setMaxWidth(ci.maxSize);
2639                if (ci.minSize == ci.maxSize)
2640                    column.setResizable(false);
2641                column.setHeaderValue(ci.showName ? ci.name : "");
2642                column.setToolTipText(descriptor.getDescription() + ci.getSuffix());
2643                column.setVisible(ci.visible);
2644                column.setSortable(true);
2645
2646                final Class<?> type = descriptor.getType();
2647
2648                // image class type column --> use a special renderer
2649                if (type == Image.class)
2650                    column.setCellRenderer(new ImageTableCellRenderer(18));
2651                else if (type == Color.class)
2652                    column.setCellRenderer(new ImageTableCellRenderer(16));
2653                // use the number cell renderer
2654                else if (ClassUtil.isSubClass(type, Number.class))
2655                    column.setCellRenderer(new SubstanceDefaultTableCellRenderer.NumberRenderer());
2656                // column.setCellRenderer(new NumberTableCellRenderer());
2657
2658                // and finally add to the model
2659                addColumn(column);
2660            }
2661
2662            setColumnSelectionAllowed(false);
2663        }
2664
2665        public void updateHeaders()
2666        {
2667            final List<ColumnInfo> columnInfos = columnInfoList;
2668            final List<TableColumn> columns = getColumns(true);
2669            for (TableColumn column : columns)
2670            {
2671                final ColumnInfo ci = getColumnInfo(columnInfos, column.getModelIndex());
2672
2673                if (ci != null)
2674                {
2675                    final ROIDescriptor descriptor = ci.descriptor;
2676
2677                    // that should be always the case
2678                    if (StringUtil.equals((String) column.getIdentifier(), descriptor.getId()))
2679                    {
2680                        column.setHeaderValue(ci.showName ? ci.name : "");
2681                        if (column instanceof TableColumnExt)
2682                            ((TableColumnExt) column).setToolTipText(descriptor.getDescription() + ci.getSuffix());
2683                    }
2684                }
2685            }
2686        }
2687    }
2688
2689    // class CustomTableCellRenderer extends DefaultTableCellRenderer
2690    // {
2691    // @Override
2692    // public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected,
2693    // boolean hasFocus, int row, int column)
2694    // {
2695    // super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);
2696    //
2697    // ROIResults roiResults;
2698    // try
2699    // {
2700    // roiResults = getROIResults(roiTable.convertRowIndexToModel(row));
2701    //
2702    // }
2703    // catch (IndexOutOfBoundsException e)
2704    // {
2705    // roiResults = null;
2706    // }
2707    //
2708    // final Color defaultColor;
2709    // final Color computeColor;
2710    //
2711    // if (isSelected)
2712    // defaultColor = UIManager.getColor("Table.selectionBackground");
2713    // else
2714    // defaultColor = UIManager.getColor("Table.background");
2715    // if (roiResults == null)
2716    // computeColor = Color.green;
2717    // else if (roiResults.areResultsUpToDate())
2718    // computeColor = Color.green;
2719    // else if (hasPendingComputation(roiResults))
2720    // computeColor = Color.orange;
2721    // else
2722    // computeColor = Color.red;
2723    //
2724    // // define background color
2725    // setBackground(ColorUtil.mix(defaultColor, computeColor, 0.15f));
2726    //
2727    // return this;
2728    // }
2729    // }
2730
2731    protected class ROITableSortController<M extends TableModel> extends DefaultSortController<M>
2732    {
2733        public ROITableSortController()
2734        {
2735            super();
2736
2737            cachedModelRowCount = roiTableModel.getRowCount();
2738            setModelWrapper(new TableRowSorterModelWrapper());
2739        }
2740
2741        @Override
2742        public void sort()
2743        {
2744            try
2745            {
2746                super.sort();
2747            }
2748            catch (Exception e)
2749            {
2750                // ignore this...
2751                // System.err.println("ROI table column sort failed:");
2752                // System.err.println(e.getMessage());
2753            }
2754        }
2755
2756        // @Override
2757        // protected void fireSortOrderChanged()
2758        // {
2759        // super.fireSortOrderChanged();
2760        //
2761        // final List<? extends SortKey> keys = getSortKeys();
2762        //
2763        // if (!keys.isEmpty())
2764        // forceComputationForColumn(keys.get(0).getColumn());
2765        // }
2766
2767        /**
2768         * Returns the <code>Comparator</code> for the specified
2769         * column. If a <code>Comparator</code> has not been specified using
2770         * the <code>setComparator</code> method a <code>Comparator</code> will be returned based on the column class
2771         * (<code>TableModel.getColumnClass</code>) of the specified column.
2772         *
2773         * @throws IndexOutOfBoundsException
2774         *         {@inheritDoc}
2775         */
2776        @Override
2777        public Comparator<?> getComparator(int column)
2778        {
2779            return comparator;
2780        }
2781
2782        /**
2783         * {@inheritDoc}
2784         * <p>
2785         * Note: must implement same logic as the overridden comparator lookup, otherwise will throw ClassCastException
2786         * because here the comparator is never null.
2787         * <p>
2788         * PENDING JW: think about implications to string value lookup!
2789         * 
2790         * @throws IndexOutOfBoundsException
2791         *         {@inheritDoc}
2792         */
2793        @Override
2794        protected boolean useToString(int column)
2795        {
2796            return false;
2797        }
2798
2799        /**
2800         * Implementation of DefaultRowSorter.ModelWrapper that delegates to a
2801         * TableModel.
2802         */
2803        private class TableRowSorterModelWrapper extends ModelWrapper<M, Integer>
2804        {
2805            public TableRowSorterModelWrapper()
2806            {
2807                super();
2808            }
2809
2810            @Override
2811            public M getModel()
2812            {
2813                return (M) roiTableModel;
2814            }
2815
2816            @Override
2817            public int getColumnCount()
2818            {
2819                return roiTableModel.getColumnCount();
2820            }
2821
2822            @Override
2823            public int getRowCount()
2824            {
2825                return roiTableModel.getRowCount();
2826            }
2827
2828            @Override
2829            public Object getValueAt(int row, int column)
2830            {
2831                return roiTableModel.getValueAt(row, column);
2832            }
2833
2834            @Override
2835            public String getStringValueAt(int row, int column)
2836            {
2837                return getStringValueProvider().getStringValue(row, column).getString(getValueAt(row, column));
2838            }
2839
2840            @Override
2841            public Integer getIdentifier(int index)
2842            {
2843                return Integer.valueOf(index);
2844            }
2845        }
2846    }
2847
2848    // protected class NumberTableCellRenderer extends SubstanceDefaultTableCellRenderer.NumberRenderer
2849    // {
2850    //
2851    // /**
2852    // *
2853    // */
2854    // private static final long serialVersionUID = 5033090596184731420L;
2855    //
2856    // @Override
2857    // public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected,
2858    // boolean hasFocus, int row, int column)
2859    // {
2860    // final Component result = super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row,
2861    // column);
2862    //
2863    // final ROIResults roiResults = getRoiResults(row);
2864    //
2865    // if (roiResults != null)
2866    // {
2867    // final ColumnInfo ci = getVisibleColumnInfo(column);
2868    //
2869    // if (ci != null)
2870    // {
2871    // final DescriptorResult descResult = roiResults.getDescriptorResult(ci);
2872    //
2873    // if (descResult != null)
2874    // {
2875    // if (descResult.isOutdated())
2876    // result.setBackground(ColorUtil.mix(Color.red, result.getBackground()));
2877    // }
2878    // }
2879    // }
2880    //
2881    // return result;
2882    // }
2883    // }
2884}