/*
 * Copyright 2010-2015 Institut Pasteur.
 * 
 * This file is part of Icy.
 * 
 * Icy is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * Icy is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with Icy. If not, see <http://www.gnu.org/licenses/>.
 */
package icy.gui.component;

import icy.action.RoiActions;
import icy.canvas.IcyCanvas;
import icy.canvas.IcyCanvas2D;
import icy.canvas.IcyCanvas3D;
import icy.gui.component.IcyTextField.TextChangeListener;
import icy.gui.component.button.IcyButton;
import icy.gui.component.renderer.ImageTableCellRenderer;
import icy.gui.inspector.RoiSettingFrame;
import icy.gui.main.ActiveSequenceListener;
import icy.gui.util.GuiUtil;
import icy.gui.util.LookAndFeelUtil;
import icy.gui.viewer.Viewer;
import icy.main.Icy;
import icy.math.MathUtil;
import icy.plugin.PluginLoader;
import icy.plugin.PluginLoader.PluginLoaderEvent;
import icy.plugin.PluginLoader.PluginLoaderListener;
import icy.plugin.interface_.PluginROIDescriptor;
import icy.preferences.XMLPreferences;
import icy.roi.ROI;
import icy.roi.ROIDescriptor;
import icy.roi.ROIEvent;
import icy.roi.ROIEvent.ROIEventType;
import icy.roi.ROIListener;
import icy.roi.ROIUtil;
import icy.sequence.Sequence;
import icy.sequence.SequenceEvent;
import icy.sequence.SequenceEvent.SequenceEventSourceType;
import icy.system.IcyExceptionHandler;
import icy.system.thread.InstanceProcessor;
import icy.system.thread.ThreadUtil;
import icy.util.ClassUtil;
import icy.util.StringUtil;

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Image;
import java.awt.Insets;
import java.awt.Point;
import java.awt.event.KeyEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;

import javax.swing.ActionMap;
import javax.swing.Box;
import javax.swing.DefaultListSelectionModel;
import javax.swing.InputMap;
import javax.swing.JComponent;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.KeyStroke;
import javax.swing.ListSelectionModel;
import javax.swing.ScrollPaneConstants;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import javax.swing.table.AbstractTableModel;
import javax.swing.table.JTableHeader;
import javax.swing.table.TableColumn;
import javax.swing.table.TableColumnModel;
import javax.swing.table.TableModel;

import org.jdesktop.swingx.JXTable;
import org.jdesktop.swingx.decorator.HighlighterFactory;
import org.jdesktop.swingx.sort.DefaultSortController;
import org.jdesktop.swingx.table.DefaultTableColumnModelExt;
import org.jdesktop.swingx.table.TableColumnExt;
import org.pushingpixels.substance.api.renderers.SubstanceDefaultTableCellRenderer;
import org.pushingpixels.substance.api.skin.SkinChangeListener;

import plugins.kernel.roi.descriptor.intensity.ROIMaxIntensityDescriptor;
import plugins.kernel.roi.descriptor.intensity.ROIMeanIntensityDescriptor;
import plugins.kernel.roi.descriptor.intensity.ROIMinIntensityDescriptor;
import plugins.kernel.roi.descriptor.intensity.ROISumIntensityDescriptor;
import plugins.kernel.roi.descriptor.measure.ROIAreaDescriptor;
import plugins.kernel.roi.descriptor.measure.ROIContourDescriptor;
import plugins.kernel.roi.descriptor.measure.ROIInteriorDescriptor;
import plugins.kernel.roi.descriptor.measure.ROIMassCenterCDescriptor;
import plugins.kernel.roi.descriptor.measure.ROIMassCenterTDescriptor;
import plugins.kernel.roi.descriptor.measure.ROIMassCenterXDescriptor;
import plugins.kernel.roi.descriptor.measure.ROIMassCenterYDescriptor;
import plugins.kernel.roi.descriptor.measure.ROIMassCenterZDescriptor;
import plugins.kernel.roi.descriptor.measure.ROIPerimeterDescriptor;
import plugins.kernel.roi.descriptor.measure.ROISurfaceAreaDescriptor;
import plugins.kernel.roi.descriptor.measure.ROIVolumeDescriptor;
import plugins.kernel.roi.descriptor.property.ROIColorDescriptor;
import plugins.kernel.roi.descriptor.property.ROIGroupIdDescriptor;
import plugins.kernel.roi.descriptor.property.ROIIconDescriptor;
import plugins.kernel.roi.descriptor.property.ROINameDescriptor;
import plugins.kernel.roi.descriptor.property.ROIOpacityDescriptor;
import plugins.kernel.roi.descriptor.property.ROIPositionCDescriptor;
import plugins.kernel.roi.descriptor.property.ROIPositionTDescriptor;
import plugins.kernel.roi.descriptor.property.ROIPositionXDescriptor;
import plugins.kernel.roi.descriptor.property.ROIPositionYDescriptor;
import plugins.kernel.roi.descriptor.property.ROIPositionZDescriptor;
import plugins.kernel.roi.descriptor.property.ROISizeCDescriptor;
import plugins.kernel.roi.descriptor.property.ROISizeTDescriptor;
import plugins.kernel.roi.descriptor.property.ROISizeXDescriptor;
import plugins.kernel.roi.descriptor.property.ROISizeYDescriptor;
import plugins.kernel.roi.descriptor.property.ROISizeZDescriptor;

/**
 * Abstract ROI panel component
 */
public abstract class AbstractRoisPanel extends ExternalizablePanel implements ActiveSequenceListener,
        TextChangeListener, ListSelectionListener, PluginLoaderListener
{
    /**
     * 
     */
    protected static final long serialVersionUID = -2870878233087117178L;

    protected static final String ID_VIEW = "view";
    protected static final String ID_EXPORT = "export";

    protected static final String ID_PROPERTY_MINSIZE = "minSize";
    protected static final String ID_PROPERTY_MAXSIZE = "maxSize";
    protected static final String ID_PROPERTY_DEFAULTSIZE = "defaultSize";
    protected static final String ID_PROPERTY_ORDER = "order";
    protected static final String ID_PROPERTY_VISIBLE = "visible";

    // default row comparator
    protected static Comparator<Object> comparator = new Comparator<Object>()
    {
        @SuppressWarnings({"unchecked", "rawtypes"})
        @Override
        public int compare(Object o1, Object o2)
        {
            if (o1 == null)
            {
                if (o2 == null)
                    return 0;
                return -1;
            }
            if (o2 == null)
                return 1;

            Object obj1 = o1;
            Object obj2 = o2;

            if (o1 instanceof String)
            {
                if (o1.equals("-" + MathUtil.INFINITE_STRING))
                    obj1 = Double.valueOf(Double.NEGATIVE_INFINITY);
                else if (o1.equals(MathUtil.INFINITE_STRING))
                    obj1 = Double.valueOf(Double.POSITIVE_INFINITY);
            }

            if (o2 instanceof String)
            {
                if (o2.equals("-" + MathUtil.INFINITE_STRING))
                    obj2 = Double.valueOf(Double.NEGATIVE_INFINITY);
                else if (o2.equals(MathUtil.INFINITE_STRING))
                    obj2 = Double.valueOf(Double.POSITIVE_INFINITY);
            }

            if ((obj1 instanceof Number) && (obj2 instanceof Number))
            {
                final double d1 = ((Number) obj1).doubleValue();
                final double d2 = ((Number) obj2).doubleValue();

                if (Double.isNaN(d1))
                {
                    if (Double.isNaN(d2))
                        return 0;
                    return -1;
                }
                if (Double.isNaN(d2))
                    return 1;

                if (d1 < d2)
                    return -1;
                if (d1 > d2)
                    return 1;

                return 0;
            }
            else if ((obj1 instanceof Comparable) && (obj1.getClass() == obj2.getClass()))
                return ((Comparable) obj1).compareTo(obj2);

            return o1.toString().compareTo(o2.toString());
        }
    };

    // GUI
    protected ROITableModel roiTableModel;
    protected ListSelectionModel roiSelectionModel;
    protected JXTable roiTable;
    protected IcyTextField nameFilter;
    protected JLabel roiNumberLabel;
    protected JLabel selectedRoiNumberLabel;

    // PluginDescriptors / ROIDescriptor map
    protected Map<ROIDescriptor, PluginROIDescriptor> descriptorMap;
    // DescriptorComputer / ROIDescriptor map
    protected Map<ROIDescriptor, DescriptorComputer> descriptorComputerMap;

    // Descriptor / column info (static to the class)
    protected List<ColumnInfo> columnInfoList;
    // // last visible columns (used to detect change in column configuration)
    // List<String> lastVisibleColumnIds;

    // ROI info list cache
    protected Set<ROI> roiSet;
    protected Map<ROI, ROIResults> roiResultsMap;
    protected List<ROI> filteredRoiList;
    protected List<ROIResults> filteredRoiResultsList;

    // internals
    protected final XMLPreferences basePreferences;
    protected final XMLPreferences viewPreferences;
    protected final XMLPreferences exportPreferences;
    protected final Semaphore modifySelection;

    // complete refresh of the roiTable
    protected final Runnable roiListRefresher;
    protected final Runnable filteredRoiListRefresher;
    protected final Runnable tableDataStructureRefresher;
    protected final Runnable tableDataRefresher;
    protected final Runnable tableSelectionRefresher;
    protected final Runnable columnInfoListRefresher;
    protected final InstanceProcessor processor;

    protected final DescriptorComputer primaryDescriptorComputer;
    protected final DescriptorComputer basicDescriptorComputer;
    protected final DescriptorComputer advancedDescriptorComputer;

    protected long lastTableDataRefresh;

    /**
     * Create a new ROI table panel.<br>
     * 
     * @param preferences
     *        XML preferences node which will contains the ROI table settings
     */
    public AbstractRoisPanel(XMLPreferences preferences)
    {
        super("ROI", "roiPanel", new Point(100, 100), new Dimension(400, 600));

        basePreferences = preferences;
        viewPreferences = basePreferences.node(ID_VIEW);
        exportPreferences = basePreferences.node(ID_EXPORT);

        roiSet = new HashSet<ROI>();
        roiResultsMap = new HashMap<ROI, ROIResults>();
        filteredRoiList = new ArrayList<ROI>();
        filteredRoiResultsList = new ArrayList<ROIResults>();
        modifySelection = new Semaphore(1);
        columnInfoList = new ArrayList<ColumnInfo>();

        lastTableDataRefresh = 0L;

        initialize();

        roiListRefresher = new Runnable()
        {
            @Override
            public void run()
            {
                refreshRoisInternal();
            }
        };
        filteredRoiListRefresher = new Runnable()
        {
            @Override
            public void run()
            {
                refreshFilteredRoisInternal();
            }
        };
        tableDataStructureRefresher = new Runnable()
        {
            @Override
            public void run()
            {
                refreshTableDataStructureInternal();
            }
        };
        tableDataRefresher = new Runnable()
        {
            @Override
            public void run()
            {
                refreshTableDataInternal();
            }
        };
        tableSelectionRefresher = new Runnable()
        {
            @Override
            public void run()
            {
                refreshTableSelectionInternal();
            }
        };
        columnInfoListRefresher = new Runnable()
        {
            @Override
            public void run()
            {
                refreshColumnInfoListInternal();
            }
        };

        LookAndFeelUtil.addSkinChangeListener(new SkinChangeListener()
        {
            @Override
            public void skinChanged()
            {
                // regenerate column model to redefine the Cell Renderer (else colors are wrong)
                roiTable.setColumnModel(new ROITableColumnModel());

                // final TableColumnModel columnModel = roiTable.getColumnModel();
                //
                // for (int i = 0; i < columnModel.getColumnCount(); i++)
                // {
                // final TableColumn column = columnModel.getColumn(i);
                //
                // // need to reset specific renderer as background color can be wrong
                // if (column.getCellRenderer() instanceof ImageTableCellRenderer)
                // column.setCellRenderer(new ImageTableCellRenderer(18));
                // }

                // modify highlighter
                roiTable.setHighlighters(HighlighterFactory.createSimpleStriping());
            }
        });

        processor = new InstanceProcessor();
        processor.setThreadName("ROI panel GUI refresher");
        processor.setKeepAliveTime(30, TimeUnit.SECONDS);

        primaryDescriptorComputer = new DescriptorComputer(DescriptorType.PRIMARY);
        basicDescriptorComputer = new DescriptorComputer(DescriptorType.BASIC);
        advancedDescriptorComputer = new DescriptorComputer(DescriptorType.EXTERNAL);
        primaryDescriptorComputer.start();
        basicDescriptorComputer.start();
        advancedDescriptorComputer.start();

        // update descriptors list (this rebuild the column model of the tree table)
        refreshDescriptorList();
        // set shortcuts
        buildActionMap();

        refreshRois();

        // listen plugin loader changes
        PluginLoader.addListener(this);
    }

    protected void initialize()
    {
        // need filter before load
        nameFilter = new IcyTextField();
        nameFilter.setToolTipText("Filter ROI by name");
        nameFilter.addTextChangeListener(this);

        selectedRoiNumberLabel = new JLabel("0");
        roiNumberLabel = new JLabel("0");

        // build roiTable model
        roiTableModel = new ROITableModel();

        // build roiTable
        roiTable = new JXTable(roiTableModel);
        roiTable.setAutoStartEditOnKeyStroke(false);
        roiTable.setAutoCreateRowSorter(false);
        roiTable.setAutoCreateColumnsFromModel(false);
        roiTable.setShowVerticalLines(false);
        roiTable.setColumnControlVisible(false);
        roiTable.setColumnSelectionAllowed(false);
        roiTable.setRowSelectionAllowed(true);
        roiTable.setSortable(true);
        // set highlight
        roiTable.setHighlighters(HighlighterFactory.createSimpleStriping());
        roiTable.addMouseListener(new MouseAdapter()
        {
            @Override
            public void mousePressed(MouseEvent event)
            {
                if (event.getClickCount() == 2)
                {
                    final int c = roiTable.columnAtPoint(event.getPoint());
                    TableColumn col = null;

                    if (c != -1)
                        col = roiTable.getColumn(c);

                    if ((col == null) || !col.getHeaderValue().equals(new ROINameDescriptor().getName()))
                        roiTableDoubleClicked();
                }
            }
        });

        // set header settings
        final JTableHeader tableHeader = roiTable.getTableHeader();
        tableHeader.setReorderingAllowed(false);
        tableHeader.setResizingAllowed(true);

        // set selection model
        roiSelectionModel = roiTable.getSelectionModel();
        roiSelectionModel.addListSelectionListener(this);
        roiSelectionModel.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);

        roiTable.setRowSorter(new ROITableSortController<ROITableModel>());

        final JPanel middlePanel = new JPanel(new BorderLayout(0, 0));

        middlePanel.add(roiTable.getTableHeader(), BorderLayout.NORTH);
        middlePanel.add(new JScrollPane(roiTable, ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS,
                ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER), BorderLayout.CENTER);

        final IcyButton settingButton = new IcyButton(RoiActions.settingAction);
        settingButton.setHideActionText(true);
        settingButton.setFlat(true);
        
        final IcyButton xlsExportButton = new IcyButton(RoiActions.xlsExportAction);
        xlsExportButton.setHideActionText(true);
        xlsExportButton.setFlat(true);

        setLayout(new BorderLayout());
        add(GuiUtil.createLineBoxPanel(nameFilter, Box.createHorizontalStrut(8), selectedRoiNumberLabel, new JLabel(
                " / "), roiNumberLabel, Box.createHorizontalStrut(4), settingButton, xlsExportButton), BorderLayout.NORTH);
        add(middlePanel, BorderLayout.CENTER);

        validate();
    }

    protected void buildActionMap()
    {
        final InputMap imap = roiTable.getInputMap(JComponent.WHEN_FOCUSED);
        final ActionMap amap = roiTable.getActionMap();

        imap.put(RoiActions.unselectAction.getKeyStroke(), RoiActions.unselectAction.getName());
        imap.put(RoiActions.deleteAction.getKeyStroke(), RoiActions.deleteAction.getName());
        // also allow backspace key for delete operation here
        imap.put(KeyStroke.getKeyStroke(KeyEvent.VK_BACK_SPACE, 0), RoiActions.deleteAction.getName());
        imap.put(RoiActions.copyAction.getKeyStroke(), RoiActions.copyAction.getName());
        imap.put(RoiActions.pasteAction.getKeyStroke(), RoiActions.pasteAction.getName());
        imap.put(RoiActions.copyLinkAction.getKeyStroke(), RoiActions.copyLinkAction.getName());
        imap.put(RoiActions.pasteLinkAction.getKeyStroke(), RoiActions.pasteLinkAction.getName());

        // disable search feature (we have our own filter)
        amap.remove("find");
        amap.put(RoiActions.unselectAction.getName(), RoiActions.unselectAction);
        amap.put(RoiActions.deleteAction.getName(), RoiActions.deleteAction);
        amap.put(RoiActions.copyAction.getName(), RoiActions.copyAction);
        amap.put(RoiActions.pasteAction.getName(), RoiActions.pasteAction);
        amap.put(RoiActions.copyLinkAction.getName(), RoiActions.copyLinkAction);
        amap.put(RoiActions.pasteLinkAction.getName(), RoiActions.pasteLinkAction);
    }

    protected ROIResults createNewROIResults(ROI roi)
    {
        return new ROIResults(roi);
    }

    /**
     * Returns number of channel of current sequence
     */
    protected int getChannelCount()
    {
        final Sequence sequence = getSequence();

        if (sequence != null)
            return sequence.getSizeC();

        return 1;
    }

    /**
     * Returns roiTable column suffix for the specified channel
     */
    protected String getChannelNameSuffix(int ch)
    {
        final Sequence sequence = getSequence();

        if ((sequence != null) && (ch < getChannelCount()))
            return " (" + sequence.getChannelName(ch) + ")";

        return "";
    }

    /**
     * Returns ROI descriptor given its id.
     */
    protected ROIDescriptor getROIDescriptor(String descriptorId)
    {
        final ROIDescriptor[] descriptors;

        synchronized (descriptorMap)
        {
            descriptors = descriptorMap.keySet().toArray(new ROIDescriptor[descriptorMap.size()]);
        }

        for (ROIDescriptor descriptor : descriptors)
            if (descriptor.getId().equals(descriptorId))
                return descriptor;

        return null;
    }

    // /**
    // * Get column info for specified visible column index.
    // */
    // protected ColumnInfo getVisibleColumnInfo(List<ColumnInfo> columns, int column)
    // {
    // int ind = 0;
    // for (int c = 0; c < columns.size(); c++)
    // {
    // final ColumnInfo col = columns.get(c);
    //
    // if (col.visible)
    // {
    // if (ind == column)
    // return col;
    // ind++;
    // }
    // }
    //
    // return null;
    // }
    //
    // /**
    // * Get column info for specified visible column index.
    // */
    // protected ColumnInfo getVisibleColumnInfo(int column)
    // {
    // return getVisibleColumnInfo(columnInfoList, column);
    // }

    /**
     * Get column info for specified column index.
     */
    protected ColumnInfo getColumnInfo(List<ColumnInfo> columns, int column)
    {
        if (column < columns.size())
            return columns.get(column);

        return null;
    }

    /**
     * Get column info for specified column index.
     */
    protected ColumnInfo getColumnInfo(int column)
    {
        return getColumnInfo(columnInfoList, column);
    }

    protected ColumnInfo getColumnInfo(List<ColumnInfo> columns, ROIDescriptor descriptor, int channel)
    {
        for (ColumnInfo ci : columns)
            if (ci.descriptor.equals(descriptor) && (ci.channel == channel))
                return ci;

        return null;
    }

    protected ColumnInfo getColumnInfo(ROIDescriptor descriptor, int channel)
    {
        return getColumnInfo(columnInfoList, descriptor, channel);
    }

    protected abstract Sequence getSequence();

    public void setNameFilter(String name)
    {
        nameFilter.setText(name);
    }

    protected boolean computeROIResults(ROIResults roiResults, Sequence seq, ColumnInfo columnInfo)
    {
        final Map<ColumnInfo, DescriptorResult> results = roiResults.descriptorResults;
        final ROIDescriptor descriptor = columnInfo.descriptor;
        final DescriptorResult result;

        synchronized (results)
        {
            // get result
            result = results.get(columnInfo);
        }

        // need to refresh this column result
        if ((result != null) && result.isOutdated())
        {
            // get the corresponding plugin
            final PluginROIDescriptor plugin;

            synchronized (descriptorMap)
            {
                plugin = descriptorMap.get(descriptor);
            }

            if (plugin != null)
            {
                final Map<ROIDescriptor, Object> newResults;

                try
                {
                    // need computation per channel ?
                    if (descriptor.separateChannel())
                    {
                        // retrieve the ROI for this channel
                        final ROI roi = roiResults.getRoiForChannel(columnInfo.channel);

                        if (roi == null)
                            throw new UnsupportedOperationException("Can't retrieve sub ROI for channel "
                                    + columnInfo.channel);

                        newResults = plugin.compute(roi, seq);
                    }
                    else
                        newResults = plugin.compute(roiResults.roi, seq);

                    for (Entry<ROIDescriptor, Object> entryNewResult : newResults.entrySet())
                    {
                        // get the column for this result
                        final ColumnInfo resultColumnInfo = getColumnInfo(entryNewResult.getKey(), columnInfo.channel);
                        final DescriptorResult oResult;

                        synchronized (results)
                        {
                            // get corresponding result
                            oResult = results.get(resultColumnInfo);
                        }

                        if (oResult != null)
                        {
                            // set the result value
                            oResult.setValue(entryNewResult.getValue());
                            // result is up to date
                            oResult.setOutdated(false);
                        }
                    }
                }
                catch (Throwable t)
                {
                    // not an UnsupportedOperationException --> show the error
                    if (!(t instanceof UnsupportedOperationException))
                        IcyExceptionHandler.handleException(t, true);

                    final List<ROIDescriptor> descriptors = plugin.getDescriptors();

                    if (descriptors != null)
                    {
                        // not supported --> clear associated results and set them as computed
                        for (ROIDescriptor desc : descriptors)
                        {
                            // get the column for this result
                            final ColumnInfo resultColumnInfo = getColumnInfo(desc, columnInfo.channel);
                            final DescriptorResult oResult;

                            synchronized (results)
                            {
                                // get corresponding result
                                oResult = results.get(resultColumnInfo);
                            }

                            if (oResult != null)
                            {
                                oResult.setValue(null);
                                oResult.setOutdated(false);
                            }
                        }
                    }
                }

                // we updated result
                return true;
            }
        }

        return false;
    }

    /**
     * Return index of specified ROI in the filtered ROI list
     */
    protected int getRoiIndex(ROI roi)
    {
        final int result = Collections.binarySearch(filteredRoiList, roi, ROI.idComparator);

        if (result >= 0)
            return result;

        return -1;
    }

    /**
     * Return index of specified ROI in the model
     */
    protected int getRoiModelIndex(ROI roi)
    {
        return getRoiIndex(roi);
    }

    /**
     * Return index of specified ROI in the table (view)
     */
    protected int getRoiViewIndex(ROI roi)
    {
        final int ind = getRoiModelIndex(roi);

        if (ind == -1)
            return ind;

        try
        {
            return roiTable.convertRowIndexToView(ind);
        }
        catch (IndexOutOfBoundsException e)
        {
            return -1;
        }
    }

    protected ROIResults getRoiResults(int rowModelIndex)
    {
        final List<ROIResults> entries = filteredRoiResultsList;

        if ((rowModelIndex >= 0) && (rowModelIndex < entries.size()))
            return entries.get(rowModelIndex);

        return null;
    }

    /**
     * Returns the visible ROI in the ROI control panel.
     */
    public List<ROI> getVisibleRois()
    {
        return new ArrayList<ROI>(filteredRoiList);
    }

    // /**
    // * Returns the ROI informations for the specified ROI.
    // */
    // public ROIInfo getROIInfo(ROI roi)
    // {
    // final int index = getRoiIndex(roi);
    //
    // if (index != -1)
    // return filteredRois.get(index);
    //
    // return null;
    // }

    /**
     * Returns the number of selected ROI from the table.
     */
    public int getSelectedRoisCount()
    {
        int result = 0;

        synchronized (roiSelectionModel)
        {
            if (!roiSelectionModel.isSelectionEmpty())
            {
                for (int i = roiSelectionModel.getMinSelectionIndex(); i <= roiSelectionModel.getMaxSelectionIndex(); i++)
                    if (roiSelectionModel.isSelectedIndex(i))
                        result++;
            }
        }

        return result;
    }

    /**
     * Returns the selected ROI from the table.
     */
    public List<ROI> getSelectedRois()
    {
        final List<ROIResults> roiResults = filteredRoiResultsList;
        final List<ROI> result = new ArrayList<ROI>(roiResults.size());

        synchronized (roiSelectionModel)
        {
            if (!roiSelectionModel.isSelectionEmpty())
            {
                for (int i = roiSelectionModel.getMinSelectionIndex(); i <= roiSelectionModel.getMaxSelectionIndex(); i++)
                {
                    if (roiSelectionModel.isSelectedIndex(i))
                    {
                        try
                        {
                            final int index = roiTable.convertRowIndexToModel(i);

                            if ((index >= 0) && (index < roiResults.size()))
                                result.add(roiResults.get(index).roi);
                        }
                        catch (IndexOutOfBoundsException e)
                        {
                            // ignore
                        }
                    }
                }
            }
        }

        return result;
    }

    /**
     * Select the specified list of ROI in the ROI Table
     */
    protected void setSelectedRoisInternal(Set<ROI> newSelected)
    {
        final List<Integer> selectedIndexes = new ArrayList<Integer>();
        final List<ROI> roiList = filteredRoiList;

        for (int i = 0; i < roiList.size(); i++)
        {
            final ROI roi = roiList.get(i);

            // HashSet provides fast "contains"
            if (newSelected.contains(roi))
            {
                int ind;

                try
                {
                    // convert model index to view index
                    ind = roiTable.convertRowIndexToView(i);
                }
                catch (IndexOutOfBoundsException e)
                {
                    ind = -1;
                }

                if (ind > -1)
                    selectedIndexes.add(Integer.valueOf(ind));
            }
        }

        synchronized (roiSelectionModel)
        {
            // start selection change
            roiSelectionModel.setValueIsAdjusting(true);
            try
            {
                // start by clearing selection
                roiSelectionModel.clearSelection();

                for (Integer index : selectedIndexes)
                    roiSelectionModel.addSelectionInterval(index.intValue(), index.intValue());
            }
            finally
            {
                // end selection change
                roiSelectionModel.setValueIsAdjusting(false);
            }
        }
    }

    protected Set<ROI> getFilteredSet(String filter)
    {
        final Set<ROI> rois = roiSet;
        final Set<ROI> result = new HashSet<ROI>();

        if (StringUtil.isEmpty(filter, true))
            result.addAll(rois);
        else
        {
            final String text = filter.trim().toLowerCase();

            // filter on name
            for (ROI roi : rois)
                if (roi.getName().toLowerCase().indexOf(text) != -1)
                    result.add(roi);
        }

        return result;
    }

    /**
     * Display the roi in the table (scroll if needed)
     */
    public void scrollTo(ROI roi)
    {
        final int index = getRoiIndex(roi);

        if (index != -1)
            roiTable.scrollRowToVisible(index);
    }

    protected void refreshRoiNumbers()
    {
        final int selectedCount = getSelectedRoisCount();
        final int roisCount = roiTable.getRowCount();

        selectedRoiNumberLabel.setText(Integer.toString(selectedCount));
        roiNumberLabel.setText(Integer.toString(roisCount));

        if (selectedCount == 0)
            selectedRoiNumberLabel.setToolTipText("No selected ROI");
        else if (selectedCount == 1)
            selectedRoiNumberLabel.setToolTipText("1 selected ROI");
        else
            selectedRoiNumberLabel.setToolTipText(selectedCount + " selected ROIs");

        if (roisCount == 0)
            roiNumberLabel.setToolTipText("No ROI");
        else if (roisCount == 1)
            roiNumberLabel.setToolTipText("1 ROI");
        else
            roiNumberLabel.setToolTipText(roisCount + " ROIs");
    }

    /**
     * refresh whole ROI list
     */
    protected void refreshRois()
    {
        processor.submit(true, roiListRefresher);
    }

    /**
     * refresh whole ROI list (internal)
     */
    protected void refreshRoisInternal()
    {
        final Set<ROI> currentRoiSet = roiSet;
        final Set<ROI> newRoiSet;
        final Sequence sequence = getSequence();

        if (sequence != null)
            newRoiSet = sequence.getROISet();
        else
            newRoiSet = new HashSet<ROI>();

        // no change --> exit
        if (newRoiSet.equals(currentRoiSet))
            return;

        final Set<ROI> removedSet = new HashSet<ROI>();

        // build removed set
        for (ROI roi : currentRoiSet)
            if (!newRoiSet.contains(roi))
                removedSet.add(roi);

        // remove from ROI entry map
        for (ROI roi : removedSet)
        {
            final ROIResults roiResults;

            // must be synchronized
            synchronized (roiResultsMap)
            {
                roiResults = roiResultsMap.remove(roi);
            }

            // cancel results computation
            if (roiResults != null)
                cancelDescriptorComputation(roiResults);
        }

        // set new ROI set
        roiSet = newRoiSet;

        // refresh filtered list now
        refreshFilteredRoisInternal();
    }

    /**
     * refresh filtered ROI list
     */
    protected void refreshFilteredRois()
    {
        processor.submit(true, filteredRoiListRefresher);
    }

    /**
     * refresh filtered ROI list (internal)
     */
    protected void refreshFilteredRoisInternal()
    {
        // get new filtered list
        final List<ROI> currentFilteredRoiList = filteredRoiList;
        final Set<ROI> newFilteredRoiSet = getFilteredSet(nameFilter.getText());

        // no change --> exit
        if (newFilteredRoiSet.equals(currentFilteredRoiList))
            return;

        // update filtered lists
        final List<ROI> newFilteredRoiList = new ArrayList<ROI>(newFilteredRoiSet);
        final List<ROIResults> newFilteredResultsList = new ArrayList<ROIResults>(newFilteredRoiList.size());

        // sort on id
        Collections.sort(newFilteredRoiList, ROI.idComparator);
        // then build filtered results list
        for (ROI roi : newFilteredRoiList)
        {
            ROIResults roiResults;

            synchronized (roiResultsMap)
            {
                // try to get the ROI results from the map first
                roiResults = roiResultsMap.get(roi);
                // and create it if needed
                if (roiResults == null)
                {
                    roiResults = createNewROIResults(roi);
                    roiResultsMap.put(roi, roiResults);
                }
            }

            newFilteredResultsList.add(roiResults);
        }

        filteredRoiList = newFilteredRoiList;
        filteredRoiResultsList = newFilteredResultsList;

        // update the table model (should always correspond to the filtered roi results list)
        refreshTableDataStructureInternal();
    }

    public void refreshTableDataStructure()
    {
        processor.submit(true, tableDataStructureRefresher);
    }

    protected void refreshTableDataStructureInternal()
    {
        // don't eat too much time on data structure refresh
        ThreadUtil.sleep(1);

        final Set<ROI> newSelectedRois;
        final Sequence sequence = getSequence();

        if (sequence != null)
            newSelectedRois = sequence.getSelectedROISet();
        else
            newSelectedRois = new HashSet<ROI>();

        ThreadUtil.invokeNow(new Runnable()
        {
            @Override
            public void run()
            {
                modifySelection.acquireUninterruptibly();
                try
                {
                    synchronized (roiTableModel)
                    {
                        try
                        {
                            // notify table data changed
                            roiTableModel.fireTableDataChanged();
                        }
                        catch (Exception e)
                        {
                            // Sorter don't like when we change data while it's sorting...
                        }
                    }

                    // selection to restore ?
                    if (!newSelectedRois.isEmpty())
                        setSelectedRoisInternal(newSelectedRois);

                    // // force loading values on sorted column
                    // final List<? extends SortKey> keys = roiTable.getRowSorter().getSortKeys();
                    // if (!keys.isEmpty())
                    // forceComputationForColumn(keys.get(0).getColumn());
                }
                finally
                {
                    modifySelection.release();
                }
            }
        });

        refreshRoiNumbers();
    }

    public void refreshTableData()
    {
        processor.submit(true, tableDataRefresher);
    }

    protected void refreshTableDataInternal()
    {
        final long time = System.currentTimeMillis();
        final boolean hasPendingTask = primaryDescriptorComputer.hasPendingComputation()
                && basicDescriptorComputer.hasPendingComputation()
                && advancedDescriptorComputer.hasPendingComputation();

        // still pending descriptor task ?
        if (hasPendingTask)
        {
            // avoid too much table data update
            if ((time - lastTableDataRefresh) < 200)
                return;
        }

        lastTableDataRefresh = time;

        // don't eat too much time on data structure refresh
        ThreadUtil.sleep(1);

        ThreadUtil.invokeNow(new Runnable()
        {
            @Override
            public void run()
            {
                final int rowCount = roiTable.getRowCount();

                // we use 'RowsUpdated' event to keep selection (DataChanged remove selection)
                if (rowCount > 0)
                {
                    // save anchor index which is lost with 'RowsUpdated' event
                    final int anchorInd = ((DefaultListSelectionModel) roiSelectionModel).getAnchorSelectionIndex();

                    synchronized (roiTableModel)
                    {
                        try
                        {
                            roiTableModel.fireTableRowsUpdated(0, rowCount - 1);
                        }
                        catch (Exception e)
                        {
                            // Sorter don't like when we change data while it's sorting...
                        }
                    }

                    // restore anchor index
                    if (anchorInd != -1)
                        ((DefaultListSelectionModel) roiSelectionModel).setAnchorSelectionIndex(anchorInd);
                }
            }
        });

        refreshRoiNumbers();
    }

    public void refreshTableSelection()
    {
        processor.submit(true, tableSelectionRefresher);
    }

    protected void refreshTableSelectionInternal()
    {
        // don't eat too much time on selection refresh
        ThreadUtil.sleep(1);

        final Set<ROI> newSelectedRois;
        final Sequence sequence = getSequence();

        if (sequence != null)
            newSelectedRois = sequence.getSelectedROISet();
        else
            newSelectedRois = new HashSet<ROI>();

        ThreadUtil.invokeNow(new Runnable()
        {
            @Override
            public void run()
            {
                modifySelection.acquireUninterruptibly();
                try
                {
                    // set selection
                    setSelectedRoisInternal(newSelectedRois);
                }
                finally
                {
                    modifySelection.release();
                }
            }
        });

        refreshRoiNumbers();
    }

    protected void refreshDescriptorList()
    {
        descriptorMap = ROIUtil.getROIDescriptors();
        refreshColumnInfoList();
    }

    public void refreshColumnInfoList()
    {
        processor.submit(true, columnInfoListRefresher);
    }

    protected void refreshColumnInfoListInternal()
    {
        // rebuild the column property list
        final List<ColumnInfo> newColumnInfos = new ArrayList<ColumnInfo>();
        final int numChannel = getChannelCount();

        for (ROIDescriptor descriptor : descriptorMap.keySet())
        {
            for (int ch = 0; ch < (descriptor.separateChannel() ? numChannel : 1); ch++)
                newColumnInfos.add(new ColumnInfo(descriptor, ch, viewPreferences, false));
        }

        // sort the list on order
        Collections.sort(newColumnInfos);
        // set new column info
        columnInfoList = newColumnInfos;
        // rebuild table columns
        ThreadUtil.invokeNow(new Runnable()
        {
            @Override
            public void run()
            {
                // regenerate column model
                roiTable.setColumnModel(new ROITableColumnModel());
            }
        });
    }

    // protected void forceComputationForColumn(int column)
    // {
    // final int numRow = roiTableModel.getRowCount();
    //
    // try
    // {
    // // force computation of the column values if needed
    // for (int i = 0; i < numRow; i++)
    // roiTableModel.getValueAt(i, column);
    // }
    // catch (Exception e)
    // {
    // // ignore
    // System.out.println(e);
    // }
    // }

    // protected boolean hasPendingComputation(ROIResults results)
    // {
    // return primaryDescriptorComputer.hasPendingComputation(results)
    // || basicDescriptorComputer.hasPendingComputation(results)
    // || advancedDescriptorComputer.hasPendingComputation(results);
    // }

    protected void requestDescriptorComputation(ROIResults results)
    {
        primaryDescriptorComputer.requestDescriptorComputation(results);
        basicDescriptorComputer.requestDescriptorComputation(results);
        advancedDescriptorComputer.requestDescriptorComputation(results);
    }

    protected void cancelDescriptorComputation(ROIResults results)
    {
        primaryDescriptorComputer.cancelDescriptorComputation(results);
        basicDescriptorComputer.cancelDescriptorComputation(results);
        advancedDescriptorComputer.cancelDescriptorComputation(results);
    }

    protected void cancelDescriptorComputation(ROI roi)
    {
        primaryDescriptorComputer.cancelDescriptorComputation(roi);
        basicDescriptorComputer.cancelDescriptorComputation(roi);
        advancedDescriptorComputer.cancelDescriptorComputation(roi);
    }

    protected void cancelAllDescriptorComputation()
    {
        primaryDescriptorComputer.cancelAllDescriptorComputation();
        basicDescriptorComputer.cancelAllDescriptorComputation();
        advancedDescriptorComputer.cancelAllDescriptorComputation();
    }

    /**
     * @deprecated Use {@link #getCSVFormattedInfos()} instead.
     */
    @Deprecated
    public String getCSVFormattedInfosOfSelectedRois()
    {
        // Check to ensure we have selected only a contiguous block of cells
        final int numcols = roiTable.getColumnCount();
        final int numrows = roiTable.getSelectedRowCount();

        // roiTable is empty --> returns empty string
        if (numrows == 0)
            return "";

        final StringBuffer sbf = new StringBuffer();
        final int[] rowsselected = roiTable.getSelectedRows();

        // column name
        for (int j = 1; j < numcols; j++)
        {
            sbf.append(roiTable.getModel().getColumnName(j));
            if (j < numcols - 1)
                sbf.append("\t");
        }
        sbf.append("\r\n");

        // then content
        for (int i = 0; i < numrows; i++)
        {
            for (int j = 1; j < numcols; j++)
            {
                final Object value = roiTable.getModel()
                        .getValueAt(roiTable.convertRowIndexToModel(rowsselected[i]), j);

                // special case of double array
                if (value instanceof double[])
                {
                    final double[] darray = (double[]) value;

                    for (int l = 0; l < darray.length; l++)
                    {
                        sbf.append(darray[l]);
                        if (l < darray.length - 1)
                            sbf.append(" ");
                    }
                }
                else
                    sbf.append(value);

                if (j < numcols - 1)
                    sbf.append("\t");
            }
            sbf.append("\r\n");
        }

        return sbf.toString();
    }

    /**
     * Returns all ROI informations in CSV format (tab separated) immediately.
     */
    public String getCSVFormattedInfos()
    {
        final List<ColumnInfo> exportColumnInfos = new ArrayList<ColumnInfo>();
        final Sequence seq = getSequence();
        final int numChannel = getChannelCount();

        // get export column informations
        for (ROIDescriptor descriptor : descriptorMap.keySet())
        {
            for (int ch = 0; ch < (descriptor.separateChannel() ? numChannel : 1); ch++)
                exportColumnInfos.add(new ColumnInfo(descriptor, ch, exportPreferences, true));
        }

        // sort the list on order
        Collections.sort(exportColumnInfos);

        final StringBuffer sbf = new StringBuffer();

        // column title
        for (ColumnInfo columnInfo : exportColumnInfos)
        {
            if (columnInfo.visible)
            {
                sbf.append(columnInfo.name);
                sbf.append("\t");
            }
        }
        sbf.append("\r\n");

        final List<ROI> rois = new ArrayList<ROI>(filteredRoiList);

        // content
        for (ROI roi : rois)
        {
            final ROIResults results = createNewROIResults(roi);
            final Map<ColumnInfo, DescriptorResult> descriptorResults = results.descriptorResults;

            // compute results
            for (ColumnInfo columnInfo : exportColumnInfos)
            {
                if (columnInfo.visible)
                {
                    // try to retrieve result for this column
                    final DescriptorResult result = descriptorResults.get(columnInfo);

                    // not yet created/computed --> create it and compute it now
                    if (result == null)
                    {
                        descriptorResults.put(columnInfo, new DescriptorResult(columnInfo));
                        computeROIResults(results, seq, columnInfo);
                    }
                }
            }

            // display results
            for (ColumnInfo columnInfo : exportColumnInfos)
            {
                if (columnInfo.visible)
                {
                    final DescriptorResult result = descriptorResults.get(columnInfo);
                    final String id = columnInfo.descriptor.getId();
                    final Object value;

                    if (result != null)
                        value = results.formatValue(result.getValue(), id);
                    else
                        value = null;

                    if (value != null)
                    {
                        // special case of icon --> use the ROI class name
                        if (StringUtil.equals(id, ROIIconDescriptor.ID))
                            sbf.append(roi.getSimpleClassName());
                        // special case of color --> use the color code
                        else if (StringUtil.equals(id, ROIColorDescriptor.ID))
                            sbf.append(String.format("%06X", Integer.valueOf(roi.getColor().getRGB() & 0xFFFFFF)));
                        else
                            sbf.append(value);
                    }

                    sbf.append("\t");
                }
            }
            sbf.append("\r\n");
        }

        return sbf.toString();
    }

    public void showSettingPanel()
    {
        // create and display the setting frame
        new RoiSettingFrame(viewPreferences, exportPreferences, new Runnable()
        {
            @Override
            public void run()
            {
                // refresh table columns
                refreshColumnInfoListInternal();
            }
        });
    }

    @Override
    public void textChanged(IcyTextField source, boolean validate)
    {
        if (source == nameFilter)
            refreshFilteredRois();
    }

    // called when selection changed in the ROI table
    @Override
    public void valueChanged(ListSelectionEvent e)
    {
        // currently changing the selection ? --> exit
        if (e.getValueIsAdjusting())
            return;
        // currently changing the selection ? --> exit
        if (roiSelectionModel.getValueIsAdjusting())
            return;

        if (modifySelection.tryAcquire())
        {
            // semaphore acquired here
            try
            {
                final List<ROI> selectedRois = getSelectedRois();
                final Sequence sequence = getSequence();

                // update selected ROI in sequence
                if (sequence != null)
                    sequence.setSelectedROIs(selectedRois);
            }
            finally
            {
                modifySelection.release();
            }
        }

        refreshRoiNumbers();
    }

    // called when a ROI has been double clicked in the ROI table
    protected void roiTableDoubleClicked()
    {
        final List<ROI> selectedRois = getSelectedRois();

        if (selectedRois.size() > 0)
        {
            final ROI selected = selectedRois.get(0);
            // get active viewer
            final Viewer v = Icy.getMainInterface().getActiveViewer();

            if ((v != null) && (selected != null))
            {
                // get canvas
                final IcyCanvas c = v.getCanvas();

                if (c instanceof IcyCanvas2D)
                {
                    // center view on selected ROI
                    ((IcyCanvas2D) c).centerOn(selected.getBounds5D().toRectangle2D().getBounds());
                }
                else if (c instanceof IcyCanvas3D)
                {
                    // center view on selected ROI
                    ((IcyCanvas3D) c).centerOn(selected.getBounds5D().toRectangle3D().toInteger());
                }
            }
        }
    }

    @Override
    public void sequenceActivated(Sequence value)
    {
        // refresh table columns
        refreshColumnInfoList();
        // refresh ROI list
        refreshRois();
    }

    @Override
    public void sequenceDeactivated(Sequence sequence)
    {
        // nothing here
    }

    @Override
    public void activeSequenceChanged(SequenceEvent event)
    {
        // we are modifying externally
        // if (modifySelection.availablePermits() == 0)
        // return;

        final SequenceEventSourceType sourceType = event.getSourceType();

        switch (sourceType)
        {
            case SEQUENCE_ROI:
                switch (event.getType())
                {
                    case ADDED:
                    case REMOVED:
                        refreshRois();
                        break;

                    case CHANGED:
                        // already handled by ROIResults directly
                        break;
                }
                break;

            case SEQUENCE_META:
                // refresh column name (unit can change when pixel size changed)
                for (ColumnInfo col : columnInfoList)
                    col.refreshName();

                // refresh column model
                final TableColumnModel model = roiTable.getColumnModel();
                if (model instanceof ROITableColumnModel)
                    ((ROITableColumnModel) model).updateHeaders();

                // don't use break, we also need to send the event to descriptors

            case SEQUENCE_DATA:
                final ROIResults[] allRoiResults;

                // get all ROI results
                synchronized (roiResultsMap)
                {
                    allRoiResults = roiResultsMap.values().toArray(new ROIResults[roiResultsMap.size()]);
                }

                // notify ROI results that sequence has changed
                for (ROIResults roiResults : allRoiResults)
                    roiResults.sequenceChanged(event);

                // refresh table data
                refreshTableData();
                break;

            case SEQUENCE_TYPE:
                // number of channel can have changed
                refreshColumnInfoList();
                break;
        }
    }

    @Override
    public void pluginLoaderChanged(PluginLoaderEvent e)
    {
        refreshDescriptorList();
    }

    protected class ROITableModel extends AbstractTableModel
    {
        /**
         * 
         */
        private static final long serialVersionUID = -6537163170625368503L;

        public ROITableModel()
        {
            super();
        }

        @Override
        public int getColumnCount()
        {
            return columnInfoList.size();
        }

        @Override
        public String getColumnName(int column)
        {
            final ColumnInfo ci = getColumnInfo(column);

            if ((ci != null) && (ci.showName))
                return ci.name;

            return "";
        }

        @Override
        public Class<?> getColumnClass(int column)
        {
            final ColumnInfo ci = getColumnInfo(column);

            if (ci != null)
                return ci.descriptor.getType();

            return String.class;
        }

        @Override
        public int getRowCount()
        {
            return filteredRoiResultsList.size();
        }

        @Override
        public Object getValueAt(int row, int column)
        {
            final ROIResults roiResults = getRoiResults(row);

            if (roiResults != null)
                return roiResults.getValueAt(column);

            return null;
        }

        @Override
        public void setValueAt(Object value, int row, int column)
        {
            final ROIResults roiResults = getRoiResults(row);

            if (roiResults != null)
                roiResults.setValueAt(value, column);
        }

        @Override
        public boolean isCellEditable(int row, int column)
        {
            final ROIResults roiResults = getRoiResults(row);

            if (roiResults != null)
                return roiResults.isEditable(column);

            return false;
        }
    }

    protected class ROIResults implements ROIListener
    {
        public final Map<ColumnInfo, DescriptorResult> descriptorResults;
        public final ROI roi;
        private final Map<Integer, WeakReference<ROI>> channelRois;

        protected ROIResults(ROI roi)
        {
            super();

            this.roi = roi;
            descriptorResults = new HashMap<ColumnInfo, DescriptorResult>();
            channelRois = new HashMap<Integer, WeakReference<ROI>>();

            // listen for ROI change event
            roi.addListener(this);
        }

        // boolean areResultsUpToDate()
        // {
        // for (DescriptorResult result : descriptorResults.values())
        // if (result.isOutdated())
        // return false;
        //
        // return true;
        // }

        private void clearChannelRois()
        {
            synchronized (channelRois)
            {
                channelRois.clear();
            }
        }

        public ROI getRoiForChannel(int channel)
        {
            final Integer key = Integer.valueOf(channel);
            WeakReference<ROI> reference;
            ROI result;

            synchronized (channelRois)
            {
                reference = channelRois.get(key);
            }

            if (reference != null)
                result = reference.get();
            else
                result = null;

            // channel ROI does not exist ?
            if (result == null)
            {
                // create it
                result = roi.getSubROI(-1, -1, channel);

                // failed ? try again
                if (result == null)
                    result = roi.getSubROI(-1, -1, channel);

                if (result != null)
                {
                    // and put it in map
                    synchronized (channelRois)
                    {
                        // we use WeakReference to not waste memory
                        channelRois.put(key, new WeakReference<ROI>(result));
                    }
                }
            }

            return result;
        }

        public boolean isEditable(int column)
        {
            final ColumnInfo ci = getColumnInfo(column);

            if (ci != null)
            {
                final ROIDescriptor descriptor = ci.descriptor;
                final String id = descriptor.getId();

                // only name and color descriptor are editable (a bit hacky)
                return id.equals(ROINameDescriptor.ID) || id.equals(ROIColorDescriptor.ID);
            }

            return false;
        }

        public Object formatValue(Object value, String id)
        {
            Object result = value;

            // format result if needed
            if (result instanceof Number)
            {
                final double doubleValue = ((Number) result).doubleValue();

                // replace 'infinity' by infinite symbol
                if (doubleValue == Double.POSITIVE_INFINITY)
                    result = MathUtil.INFINITE_STRING;
                else if (doubleValue == Double.NEGATIVE_INFINITY)
                {
                    // position descriptor ? negative infinite means 'ALL' here
                    if (id.equals(ROIPositionXDescriptor.ID) || id.equals(ROIPositionYDescriptor.ID)
                            || id.equals(ROIPositionZDescriptor.ID) || id.equals(ROIPositionTDescriptor.ID)
                            || id.equals(ROIPositionCDescriptor.ID) || id.equals(ROIMassCenterXDescriptor.ID)
                            || id.equals(ROIMassCenterYDescriptor.ID) || id.equals(ROIMassCenterZDescriptor.ID)
                            || id.equals(ROIMassCenterTDescriptor.ID) || id.equals(ROIMassCenterCDescriptor.ID))
                        result = "ALL";
                    else
                        result = "-" + MathUtil.INFINITE_STRING;
                }
                else if (doubleValue == -1d)
                {
                    // position descriptor ? -1 means 'ALL' here
                    if (id.equals(ROIPositionXDescriptor.ID) || id.equals(ROIPositionYDescriptor.ID)
                            || id.equals(ROIPositionZDescriptor.ID) || id.equals(ROIPositionTDescriptor.ID)
                            || id.equals(ROIPositionCDescriptor.ID) || id.equals(ROIMassCenterXDescriptor.ID)
                            || id.equals(ROIMassCenterYDescriptor.ID) || id.equals(ROIMassCenterZDescriptor.ID)
                            || id.equals(ROIMassCenterTDescriptor.ID) || id.equals(ROIMassCenterCDescriptor.ID))
                        result = "ALL";
                }
                else
                {
                    // value not too large ?
                    if (Math.abs(doubleValue) < 10000000)
                    {
                        // simple integer ? -> show it as integer
                        if (doubleValue == (int) doubleValue)
                            result = Integer.valueOf((int) doubleValue);
                        // small integer value ?
                        else if (Math.abs(doubleValue) < 100)
                            result = Double.valueOf(MathUtil.roundSignificant(doubleValue, 5));
                        // medium integer value ?
                        else if (Math.abs(doubleValue) < 10000)
                            result = Double.valueOf(MathUtil.round(doubleValue, 2));
                        // medium large integer value ?
                        else if (Math.abs(doubleValue) < 1000000)
                            result = Double.valueOf(MathUtil.round(doubleValue, 1));
                        else
                            // large integer value ?
                            result = Integer.valueOf((int) Math.round(doubleValue));
                    }
                    else
                        // format double value
                        result = Double.valueOf(MathUtil.roundSignificant(doubleValue, 5));
                }
            }

            return result;
        }

        /**
         * Retrieve the DescriptorResult for the specified column
         */
        public DescriptorResult getDescriptorResult(ColumnInfo column)
        {
            // get result for this descriptor
            DescriptorResult result;

            synchronized (descriptorResults)
            {
                result = descriptorResults.get(column);

                // no result --> create it and request computation
                if (result == null)
                {
                    // create descriptor result
                    result = new DescriptorResult(column);
                    // and put it in results map
                    descriptorResults.put(column, result);
                }
            }

            return result;
        }

        /**
         * Retrieve the value for the specified descriptor
         */
        public Object getValue(ColumnInfo column)
        {
            // get result for this descriptor
            final DescriptorResult result = getDescriptorResult(column);

            // out dated result ? --> request for descriptor computation
            if (result.isOutdated())
                requestDescriptorComputation(this);

            return formatValue(result.getValue(), column.descriptor.getId());
        }

        public Object getValueAt(int column)
        {
            final ColumnInfo ci = getColumnInfo(column);

            if (ci != null)
                return getValue(ci);

            return null;
        }

        public void setValueAt(Object aValue, int column)
        {
            final ColumnInfo ci = getColumnInfo(column);

            if (ci != null)
            {
                final ROIDescriptor descriptor = ci.descriptor;
                final String id = descriptor.getId();

                // only name descriptor is editable (a bit hacky)
                if (id.equals(ROINameDescriptor.ID))
                    roi.setName((String) aValue);
                else if (id.equals(ROIColorDescriptor.ID))
                    roi.setColor((Color) aValue);
            }
        }

        @Override
        public void roiChanged(ROIEvent event)
        {
            switch (event.getType())
            {
                case ROI_CHANGED:
                case PROPERTY_CHANGED:
                    final Object[] entries;

                    synchronized (descriptorResults)
                    {
                        entries = descriptorResults.entrySet().toArray();
                    }

                    for (Object entryObj : entries)
                    {
                        final Entry<ColumnInfo, DescriptorResult> entry = (Entry<ColumnInfo, DescriptorResult>) entryObj;
                        final ColumnInfo key = entry.getKey();
                        final ROIDescriptor descriptor = key.descriptor;

                        // need to recompute this descriptor ?
                        if (descriptor.needRecompute(event))
                        {
                            final DescriptorResult result = entry.getValue();

                            // mark as outdated
                            if (result != null)
                                result.setOutdated(true);
                        }
                    }

                    // need to recompute channel rois
                    if (event.getType() == ROIEventType.ROI_CHANGED)
                        clearChannelRois();

                    // and refresh table data
                    refreshTableData();
                    break;

                case SELECTION_CHANGED:
                    // not modifying selection from panel ?
                    if (modifySelection.availablePermits() > 0)
                        // update ROI selection
                        refreshTableSelection();
                    break;
            }
        }

        /**
         * Called when the sequence changed, in which case we need to invalidate results.
         * 
         * @param event
         *        Sequence change event
         */
        public void sequenceChanged(SequenceEvent event)
        {
            final Object[] entries;

            synchronized (descriptorResults)
            {
                entries = descriptorResults.entrySet().toArray();
            }

            for (Object entryObj : entries)
            {
                final Entry<ColumnInfo, DescriptorResult> entry = (Entry<ColumnInfo, DescriptorResult>) entryObj;
                final ColumnInfo key = entry.getKey();
                final ROIDescriptor descriptor = key.descriptor;

                // need to recompute this descriptor ?
                if (descriptor.needRecompute(event))
                {
                    final DescriptorResult result = entry.getValue();

                    // mark as outdated
                    if (result != null)
                        result.setOutdated(true);
                }
            }
        }
    }

    protected class DescriptorComputer extends Thread
    {
        protected final LinkedHashSet<ROIResults> resultsToCompute;
        protected final DescriptorType type;

        public DescriptorComputer(DescriptorType type)
        {
            super("ROI " + type.toString() + " descriptor calculator");

            resultsToCompute = new LinkedHashSet<AbstractRoisPanel.ROIResults>(256);
            this.type = type;

            setPriority(Thread.MIN_PRIORITY);
        }

        public boolean hasPendingComputation()
        {
            return resultsToCompute.size() > 0;
        }

        public boolean hasPendingComputation(ROIResults results)
        {
            synchronized (resultsToCompute)
            {
                return resultsToCompute.contains(results);
            }
        }

        public void requestDescriptorComputation(ROIResults results)
        {
            synchronized (resultsToCompute)
            {
                resultsToCompute.add(results);
                resultsToCompute.notifyAll();
            }
        }

        public void cancelDescriptorComputation(ROIResults roiResults)
        {
            synchronized (resultsToCompute)
            {
                resultsToCompute.remove(roiResults);
                resultsToCompute.notifyAll();
            }
        }

        public void cancelDescriptorComputation(ROI roi)
        {
            synchronized (resultsToCompute)
            {
                final Iterator<ROIResults> it = resultsToCompute.iterator();

                while (it.hasNext())
                {
                    final ROIResults roiResults = it.next();

                    // remove all results for this ROI
                    if (roiResults.roi == roi)
                        it.remove();
                }

                resultsToCompute.notifyAll();
            }
        }

        public void cancelAllDescriptorComputation()
        {
            synchronized (resultsToCompute)
            {
                resultsToCompute.clear();
                resultsToCompute.notifyAll();
            }
        }

        @Override
        public void run()
        {
            while (!Thread.interrupted())
            {
                final ROIResults[] roiResultsList;

                synchronized (resultsToCompute)
                {
                    try
                    {
                        while (resultsToCompute.isEmpty())
                            resultsToCompute.wait();
                    }
                    catch (InterruptedException e)
                    {
                        // ignore and just interrupt now
                        Thread.currentThread().interrupt();
                    }

                    // get results to compute
                    roiResultsList = resultsToCompute.toArray(new ROIResults[resultsToCompute.size()]);
                    // and remove them
                    resultsToCompute.clear();
                }

                final Sequence seq = getSequence();

                if (seq != null)
                {
                    // start with primaries descriptors
                    for (ROIResults roiResults : roiResultsList)
                        computeROIResults(roiResults, seq);
                }
            }
        }

        protected void computeROIResults(ROIResults roiResults, Sequence seq)
        {
            final Map<ColumnInfo, DescriptorResult> results = roiResults.descriptorResults;
            final ColumnInfo[] columnInfos;

            synchronized (results)
            {
                columnInfos = results.keySet().toArray(new ColumnInfo[results.size()]);
            }

            boolean needUpdate = false;
            for (ColumnInfo columnInfo : columnInfos)
            {
                // only compute a specific kind of descriptor
                if (columnInfo.getDescriptorType() == type)
                    needUpdate |= AbstractRoisPanel.this.computeROIResults(roiResults, seq, columnInfo);
            }

            // need to refresh data
            if (needUpdate)
                refreshTableData();
        }
    }

    protected class DescriptorResult
    {
        private Object value;
        private boolean outdated;

        public DescriptorResult(ColumnInfo column)
        {
            super();

            value = null;

            // by default we consider it as out dated
            outdated = true;
        }

        public Object getValue()
        {
            return value;
        }

        public void setValue(Object value)
        {
            this.value = value;
        }

        public boolean isOutdated()
        {
            return outdated;
        }

        public void setOutdated(boolean value)
        {
            outdated = value;
        }
    }

    public static enum DescriptorType
    {
        PRIMARY, BASIC, EXTERNAL
    };

    public static class BaseColumnInfo implements Comparable<BaseColumnInfo>
    {
        public final ROIDescriptor descriptor;
        public int minSize;
        public int maxSize;
        public int defaultSize;
        public int order;
        public boolean visible;

        public BaseColumnInfo(ROIDescriptor descriptor, XMLPreferences preferences, boolean export)
        {
            super();

            this.descriptor = descriptor;

            load(preferences, export);
        }

        public boolean load(XMLPreferences preferences, boolean export)
        {
            final XMLPreferences p = preferences.node(descriptor.getId());

            if (p != null)
            {
                minSize = p.getInt(ID_PROPERTY_MINSIZE, getDefaultMinSize());
                maxSize = p.getInt(ID_PROPERTY_MAXSIZE, getDefaultMaxSize());
                defaultSize = p.getInt(ID_PROPERTY_DEFAULTSIZE, getDefaultDefaultSize());
                order = p.getInt(ID_PROPERTY_ORDER, getDefaultOrder());
                visible = p.getBoolean(ID_PROPERTY_VISIBLE, getDefaultVisible(export));

                return true;
            }

            return false;
        }

        public boolean save(XMLPreferences preferences)
        {
            final XMLPreferences p = preferences.node(descriptor.getId());

            if (p != null)
            {
                // p.putInt(ID_PROPERTY_MINSIZE, minSize);
                // p.putInt(ID_PROPERTY_MAXSIZE, maxSize);
                // p.putInt(ID_PROPERTY_DEFAULTSIZE, defaultSize);
                p.putInt(ID_PROPERTY_ORDER, order);
                p.putBoolean(ID_PROPERTY_VISIBLE, visible);

                return true;
            }

            return false;
        }

        protected boolean getDefaultVisible(boolean export)
        {
            if (descriptor == null)
                return false;

            final String id = descriptor.getId();

            if (export)
            {
                if (StringUtil.equals(id, ROIOpacityDescriptor.ID))
                    return false;

                final Class<?> type = descriptor.getType();
                return ClassUtil.isSubClass(type, String.class) || ClassUtil.isSubClass(type, Number.class);
            }

            if (StringUtil.equals(id, ROIIconDescriptor.ID))
                return true;
            if (StringUtil.equals(id, ROINameDescriptor.ID))
                return true;

            if (StringUtil.equals(id, ROIContourDescriptor.ID))
                return true;
            if (StringUtil.equals(id, ROIInteriorDescriptor.ID))
                return true;

            return false;
        }

        protected int getDefaultOrder()
        {
            if (descriptor == null)
                return Integer.MAX_VALUE;

            final String id = descriptor.getId();
            int order = -1;

            order++;
            if (StringUtil.equals(id, ROIIconDescriptor.ID))
                return order;
            order++;
            if (StringUtil.equals(id, ROIColorDescriptor.ID))
                return order;
            order++;
            if (StringUtil.equals(id, ROIGroupIdDescriptor.ID))
                return order;
            order++;
            if (StringUtil.equals(id, ROINameDescriptor.ID))
                return order;

            order++;
            if (StringUtil.equals(id, ROIPositionXDescriptor.ID))
                return order;
            order++;
            if (StringUtil.equals(id, ROIPositionYDescriptor.ID))
                return order;
            order++;
            if (StringUtil.equals(id, ROIPositionZDescriptor.ID))
                return order;
            order++;
            if (StringUtil.equals(id, ROIPositionTDescriptor.ID))
                return order;
            order++;
            if (StringUtil.equals(id, ROIPositionCDescriptor.ID))
                return order;

            order++;
            if (StringUtil.equals(id, ROISizeXDescriptor.ID))
                return order;
            order++;
            if (StringUtil.equals(id, ROISizeYDescriptor.ID))
                return order;
            order++;
            if (StringUtil.equals(id, ROISizeZDescriptor.ID))
                return order;
            order++;
            if (StringUtil.equals(id, ROISizeTDescriptor.ID))
                return order;
            order++;
            if (StringUtil.equals(id, ROISizeCDescriptor.ID))
                return order;

            order++;
            if (StringUtil.equals(id, ROIMassCenterXDescriptor.ID))
                return order;
            order++;
            if (StringUtil.equals(id, ROIMassCenterYDescriptor.ID))
                return order;
            order++;
            if (StringUtil.equals(id, ROIMassCenterZDescriptor.ID))
                return order;
            order++;
            if (StringUtil.equals(id, ROIMassCenterTDescriptor.ID))
                return order;
            order++;
            if (StringUtil.equals(id, ROIMassCenterCDescriptor.ID))
                return order;

            order++;
            if (StringUtil.equals(id, ROIContourDescriptor.ID))
                return order;
            order++;
            if (StringUtil.equals(id, ROIInteriorDescriptor.ID))
                return order;

            order++;
            if (StringUtil.equals(id, ROIPerimeterDescriptor.ID))
                return order;
            order++;
            if (StringUtil.equals(id, ROIAreaDescriptor.ID))
                return order;
            order++;
            if (StringUtil.equals(id, ROISurfaceAreaDescriptor.ID))
                return order;
            order++;
            if (StringUtil.equals(id, ROIVolumeDescriptor.ID))
                return order;

            order++;
            if (StringUtil.equals(id, ROIMinIntensityDescriptor.ID))
                return order;
            order++;
            if (StringUtil.equals(id, ROIMeanIntensityDescriptor.ID))
                return order;
            order++;
            if (StringUtil.equals(id, ROIMaxIntensityDescriptor.ID))
                return order;
            order++;
            if (StringUtil.equals(id, ROISumIntensityDescriptor.ID))
                return order;

            return Integer.MAX_VALUE;
        }

        protected int getDefaultMinSize()
        {
            if (descriptor == null)
                return Integer.MAX_VALUE;

            final String id = descriptor.getId();

            if (StringUtil.equals(id, ROIIconDescriptor.ID))
                return 22;
            if (StringUtil.equals(id, ROIColorDescriptor.ID))
                return 18;
            if (StringUtil.equals(id, ROIGroupIdDescriptor.ID))
                return 18;
            if (StringUtil.equals(id, ROINameDescriptor.ID))
                return 60;

            final Class<?> type = descriptor.getType();

            if (type == Integer.class)
                return 30;
            if (type == Float.class)
                return 40;
            if (type == Double.class)
                return 40;
            if (type == String.class)
                return 50;

            return 40;
        }

        protected int getDefaultMaxSize()
        {
            if (descriptor == null)
                return Integer.MAX_VALUE;

            final String id = descriptor.getId();

            if (StringUtil.equals(id, ROIIconDescriptor.ID))
                return 22;
            if (StringUtil.equals(id, ROIColorDescriptor.ID))
                return 18;
            if (StringUtil.equals(id, ROIGroupIdDescriptor.ID))
                return 18;

            return Integer.MAX_VALUE;
        }

        protected int getDefaultDefaultSize()
        {
            final int maxSize = getDefaultMaxSize();
            final int minSize = getDefaultMinSize();

            if (maxSize == Integer.MAX_VALUE)
                return minSize * 2;

            return (minSize + maxSize) / 2;
        }

        /**
         * Used to know if this is a primary (name, color...) ROI descriptor.
         * 
         * @see #isBasicDescriptor()
         * @see #isExtendedDescriptor()
         * @see #getDescriptorType()
         */
        protected boolean isPrimaryDescriptor()
        {
            if (descriptor == null)
                return false;

            final String id = descriptor.getId();

            return (StringUtil.equals(id, ROIIconDescriptor.ID)) || (StringUtil.equals(id, ROIColorDescriptor.ID))
                    || (StringUtil.equals(id, ROINameDescriptor.ID))
                    || (StringUtil.equals(id, ROIGroupIdDescriptor.ID))
                    || (StringUtil.equals(id, ROIPositionXDescriptor.ID))
                    || (StringUtil.equals(id, ROIPositionYDescriptor.ID))
                    || (StringUtil.equals(id, ROIPositionZDescriptor.ID))
                    || (StringUtil.equals(id, ROIPositionTDescriptor.ID))
                    || (StringUtil.equals(id, ROIPositionCDescriptor.ID))
                    || (StringUtil.equals(id, ROISizeXDescriptor.ID)) || (StringUtil.equals(id, ROISizeYDescriptor.ID))
                    || (StringUtil.equals(id, ROISizeZDescriptor.ID)) || (StringUtil.equals(id, ROISizeTDescriptor.ID))
                    || (StringUtil.equals(id, ROISizeCDescriptor.ID));
        }

        /**
         * Used to know if this is a primary or basic (interior, contour, intensities..) ROI descriptor.
         * 
         * @see #isPrimaryDescriptor()
         * @see #isExtendedDescriptor()
         * @see #getDescriptorType()
         */
        protected boolean isBasicDescriptor()
        {
            if (descriptor == null)
                return false;

            final String id = descriptor.getId();

            return isPrimaryDescriptor() || (StringUtil.equals(id, ROIMassCenterXDescriptor.ID))
                    || (StringUtil.equals(id, ROIMassCenterYDescriptor.ID))
                    || (StringUtil.equals(id, ROIMassCenterZDescriptor.ID))
                    || (StringUtil.equals(id, ROIMassCenterTDescriptor.ID))
                    || (StringUtil.equals(id, ROIMassCenterCDescriptor.ID))
                    || (StringUtil.equals(id, ROIContourDescriptor.ID))
                    || (StringUtil.equals(id, ROIInteriorDescriptor.ID))
                    || (StringUtil.equals(id, ROIPerimeterDescriptor.ID))
                    || (StringUtil.equals(id, ROIAreaDescriptor.ID))
                    || (StringUtil.equals(id, ROISurfaceAreaDescriptor.ID))
                    || (StringUtil.equals(id, ROIVolumeDescriptor.ID))
                    || (StringUtil.equals(id, ROIMinIntensityDescriptor.ID))
                    || (StringUtil.equals(id, ROIMeanIntensityDescriptor.ID))
                    || (StringUtil.equals(id, ROIMaxIntensityDescriptor.ID));
        }

        /**
         * Used to know if this is a extended (added by an external plugin) ROI descriptor
         * 
         * @see #isPrimaryDescriptor()
         * @see #isBasicDescriptor()
         * @see #getDescriptorType()
         */
        protected boolean isExtendedDescriptor()
        {
            if (descriptor == null)
                return false;

            return !isBasicDescriptor();
        }

        /**
         * Returns the kind of ROI descriptor
         */
        public DescriptorType getDescriptorType()
        {
            if (descriptor == null)
                return null;

            if (isPrimaryDescriptor())
                return DescriptorType.PRIMARY;
            if (isBasicDescriptor())
                return DescriptorType.BASIC;

            return DescriptorType.EXTERNAL;
        }

        @Override
        public int compareTo(BaseColumnInfo obj)
        {
            return Integer.valueOf(order).compareTo(Integer.valueOf(obj.order));
        }

        @Override
        public int hashCode()
        {
            return descriptor.hashCode();
        }

        @Override
        public boolean equals(Object obj)
        {
            if (obj instanceof BaseColumnInfo)
                // equality on descriptor
                return ((BaseColumnInfo) obj).descriptor.equals(descriptor);

            return super.equals(obj);
        }
    }

    protected class ColumnInfo extends BaseColumnInfo
    {
        boolean showName;
        String name;
        final int channel;

        public ColumnInfo(ROIDescriptor descriptor, int channel, XMLPreferences prefs, boolean export)
        {
            super(descriptor, prefs, export);

            this.channel = channel;
            refreshName();
        }

        protected String getSuffix()
        {
            String result = "";

            final String unit = descriptor.getUnit(getSequence());

            if (!StringUtil.isEmpty(unit))
                result += " (" + unit + ")";

            // separate channel
            if (descriptor.separateChannel())
                result += getChannelNameSuffix(channel);

            return result;
        }

        protected void refreshName()
        {
            name = descriptor.getName() + getSuffix();

            final String id = descriptor.getId();

            // we don't want to display name for these descriptors
            if (StringUtil.equals(id, ROIIconDescriptor.ID) || StringUtil.equals(id, ROIColorDescriptor.ID)
                    || StringUtil.equals(id, ROIGroupIdDescriptor.ID))
                showName = false;
            else
                showName = true;
        }

        @Override
        public int hashCode()
        {
            return descriptor.hashCode() ^ channel;
        }

        @Override
        public boolean equals(Object obj)
        {
            if (obj instanceof ColumnInfo)
            {
                final ColumnInfo ci = (ColumnInfo) obj;

                // equality on descriptor and channel number
                return (ci.descriptor.equals(descriptor) && (ci.channel == ci.channel));
            }

            return super.equals(obj);
        }
    }

    protected class ROITableColumnModel extends DefaultTableColumnModelExt
    {
        /**
         * 
         */
        private static final long serialVersionUID = -8024047283485991234L;

        public ROITableColumnModel()
        {
            super();

            final List<ColumnInfo> columnInfos = columnInfoList;

            // column info are sorted on their order
            int index = 0;
            for (ColumnInfo ci : columnInfos)
            {
                final ROIDescriptor descriptor = ci.descriptor;
                final TableColumnExt column = new TableColumnExt(index++);

                column.setIdentifier(descriptor.getId());
                column.setMinWidth(ci.minSize);
                column.setPreferredWidth(ci.defaultSize);
                if (ci.maxSize != Integer.MAX_VALUE)
                    column.setMaxWidth(ci.maxSize);
                if (ci.minSize == ci.maxSize)
                    column.setResizable(false);
                column.setHeaderValue(ci.showName ? ci.name : "");
                column.setToolTipText(descriptor.getDescription() + ci.getSuffix());
                column.setVisible(ci.visible);
                column.setSortable(true);

                final Class<?> type = descriptor.getType();

                // image class type column --> use a special renderer
                if (type == Image.class)
                    column.setCellRenderer(new ImageTableCellRenderer(18));
                else if (type == Color.class)
                    column.setCellRenderer(new ImageTableCellRenderer(16));
                // use the number cell renderer
                else if (ClassUtil.isSubClass(type, Number.class))
                    column.setCellRenderer(new SubstanceDefaultTableCellRenderer.NumberRenderer());
                // column.setCellRenderer(new NumberTableCellRenderer());

                // and finally add to the model
                addColumn(column);
            }

            setColumnSelectionAllowed(false);
        }

        public void updateHeaders()
        {
            final List<ColumnInfo> columnInfos = columnInfoList;
            final List<TableColumn> columns = getColumns(true);
            for (TableColumn column : columns)
            {
                final ColumnInfo ci = getColumnInfo(columnInfos, column.getModelIndex());

                if (ci != null)
                {
                    final ROIDescriptor descriptor = ci.descriptor;

                    // that should be always the case
                    if (StringUtil.equals((String) column.getIdentifier(), descriptor.getId()))
                    {
                        column.setHeaderValue(ci.showName ? ci.name : "");
                        if (column instanceof TableColumnExt)
                            ((TableColumnExt) column).setToolTipText(descriptor.getDescription() + ci.getSuffix());
                    }
                }
            }
        }
    }

    // class CustomTableCellRenderer extends DefaultTableCellRenderer
    // {
    // @Override
    // public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected,
    // boolean hasFocus, int row, int column)
    // {
    // super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);
    //
    // ROIResults roiResults;
    // try
    // {
    // roiResults = getROIResults(roiTable.convertRowIndexToModel(row));
    //
    // }
    // catch (IndexOutOfBoundsException e)
    // {
    // roiResults = null;
    // }
    //
    // final Color defaultColor;
    // final Color computeColor;
    //
    // if (isSelected)
    // defaultColor = UIManager.getColor("Table.selectionBackground");
    // else
    // defaultColor = UIManager.getColor("Table.background");
    // if (roiResults == null)
    // computeColor = Color.green;
    // else if (roiResults.areResultsUpToDate())
    // computeColor = Color.green;
    // else if (hasPendingComputation(roiResults))
    // computeColor = Color.orange;
    // else
    // computeColor = Color.red;
    //
    // // define background color
    // setBackground(ColorUtil.mix(defaultColor, computeColor, 0.15f));
    //
    // return this;
    // }
    // }

    protected class ROITableSortController<M extends TableModel> extends DefaultSortController<M>
    {
        public ROITableSortController()
        {
            super();

            cachedModelRowCount = roiTableModel.getRowCount();
            setModelWrapper(new TableRowSorterModelWrapper());
        }

        @Override
        public void sort()
        {
            try
            {
                super.sort();
            }
            catch (Exception e)
            {
                // ignore this...
                // System.err.println("ROI table column sort failed:");
                // System.err.println(e.getMessage());
            }
        }

        // @Override
        // protected void fireSortOrderChanged()
        // {
        // super.fireSortOrderChanged();
        //
        // final List<? extends SortKey> keys = getSortKeys();
        //
        // if (!keys.isEmpty())
        // forceComputationForColumn(keys.get(0).getColumn());
        // }

        /**
         * Returns the <code>Comparator</code> for the specified
         * column. If a <code>Comparator</code> has not been specified using
         * the <code>setComparator</code> method a <code>Comparator</code> will be returned based on the column class
         * (<code>TableModel.getColumnClass</code>) of the specified column.
         *
         * @throws IndexOutOfBoundsException
         *         {@inheritDoc}
         */
        @Override
        public Comparator<?> getComparator(int column)
        {
            return comparator;
        }

        /**
         * {@inheritDoc}
         * <p>
         * Note: must implement same logic as the overridden comparator lookup, otherwise will throw ClassCastException
         * because here the comparator is never null.
         * <p>
         * PENDING JW: think about implications to string value lookup!
         * 
         * @throws IndexOutOfBoundsException
         *         {@inheritDoc}
         */
        @Override
        protected boolean useToString(int column)
        {
            return false;
        }

        /**
         * Implementation of DefaultRowSorter.ModelWrapper that delegates to a
         * TableModel.
         */
        private class TableRowSorterModelWrapper extends ModelWrapper<M, Integer>
        {
            public TableRowSorterModelWrapper()
            {
                super();
            }

            @Override
            public M getModel()
            {
                return (M) roiTableModel;
            }

            @Override
            public int getColumnCount()
            {
                return roiTableModel.getColumnCount();
            }

            @Override
            public int getRowCount()
            {
                return roiTableModel.getRowCount();
            }

            @Override
            public Object getValueAt(int row, int column)
            {
                return roiTableModel.getValueAt(row, column);
            }

            @Override
            public String getStringValueAt(int row, int column)
            {
                return getStringValueProvider().getStringValue(row, column).getString(getValueAt(row, column));
            }

            @Override
            public Integer getIdentifier(int index)
            {
                return Integer.valueOf(index);
            }
        }
    }

    // protected class NumberTableCellRenderer extends SubstanceDefaultTableCellRenderer.NumberRenderer
    // {
    //
    // /**
    // *
    // */
    // private static final long serialVersionUID = 5033090596184731420L;
    //
    // @Override
    // public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected,
    // boolean hasFocus, int row, int column)
    // {
    // final Component result = super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row,
    // column);
    //
    // final ROIResults roiResults = getRoiResults(row);
    //
    // if (roiResults != null)
    // {
    // final ColumnInfo ci = getVisibleColumnInfo(column);
    //
    // if (ci != null)
    // {
    // final DescriptorResult descResult = roiResults.getDescriptorResult(ci);
    //
    // if (descResult != null)
    // {
    // if (descResult.isOutdated())
    // result.setBackground(ColorUtil.mix(Color.red, result.getBackground()));
    // }
    // }
    // }
    //
    // return result;
    // }
    // }
}