/*
 * Copyright 2010-2013 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.inspector;

import icy.action.RoiActions;
import icy.gui.component.ExternalizablePanel;
import icy.gui.component.IcyTextField;
import icy.gui.component.IcyTextField.TextChangeListener;
import icy.gui.component.renderer.ImageTableCellRenderer;
import icy.gui.main.ActiveSequenceListener;
import icy.gui.util.GuiUtil;
import icy.gui.util.LookAndFeelUtil;
import icy.image.IntensityInfo;
import icy.main.Icy;
import icy.math.MathUtil;
import icy.preferences.GeneralPreferences;
import icy.preferences.XMLPreferences;
import icy.roi.ROI;
import icy.roi.ROIEvent;
import icy.roi.ROIListener;
import icy.roi.ROIUtil;
import icy.sequence.Sequence;
import icy.sequence.SequenceEvent;
import icy.sequence.SequenceEvent.SequenceEventSourceType;
import icy.sequence.SequenceEvent.SequenceEventType;
import icy.system.thread.ThreadUtil;
import icy.type.collection.CollectionUtil;
import icy.type.rectangle.Rectangle5D;
import icy.util.StringUtil;

import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.Image;
import java.awt.Point;
import java.awt.event.KeyEvent;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.Semaphore;

import javax.swing.ActionMap;
import javax.swing.Box;
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.TableColumn;

import org.jdesktop.swingx.JXTreeTable;
import org.jdesktop.swingx.decorator.HighlighterFactory;
import org.jdesktop.swingx.table.ColumnControlButton;
import org.jdesktop.swingx.table.TableColumnExt;
import org.jdesktop.swingx.table.TableColumnModelExt;
import org.jdesktop.swingx.treetable.AbstractTreeTableModel;
import org.pushingpixels.substance.api.skin.SkinChangeListener;

/**
 * @author Stephane
 */
public class RoisPanel extends ExternalizablePanel implements ActiveSequenceListener, TextChangeListener,
        ListSelectionListener, Runnable, PropertyChangeListener
{
    /**
     * 
     */
    private static final long serialVersionUID = -2870878233087117178L;

    private static final String PREF_ID = "ROIPanel";

    private static final String ID_COLUMN_ICON = "col_icon";
    private static final String ID_COLUMN_NAME = "col_name";
    private static final String ID_COLUMN_TYPE = "col_type";
    private static final String ID_COLUMN_POSITION_X = "position_x";
    private static final String ID_COLUMN_POSITION_Y = "position_y";
    private static final String ID_COLUMN_POSITION_Z = "position_z";
    private static final String ID_COLUMN_POSITION_T = "position_t";
    private static final String ID_COLUMN_POSITION_C = "position_c";
    private static final String ID_COLUMN_SIZE_X = "size_x";
    private static final String ID_COLUMN_SIZE_Y = "size_y";
    private static final String ID_COLUMN_SIZE_Z = "size_z";
    private static final String ID_COLUMN_SIZE_T = "size_t";
    private static final String ID_COLUMN_SIZE_C = "size_c";
    private static final String ID_COLUMN_CONTOUR = "col_contour";
    private static final String ID_COLUMN_POINTS = "col_points";
    private static final String ID_COLUMN_PERIMETER = "col_perimeter";
    private static final String ID_COLUMN_AREA = "col_area";
    private static final String ID_COLUMN_SURFACE_AREA = "col_surface_area";
    private static final String ID_COLUMN_VOLUME = "col_volume";
    private static final String ID_COLUMN_MIN_INT = "col_min_int";
    private static final String ID_COLUMN_MEAN_INT = "col_mean_int";
    private static final String ID_COLUMN_MAX_INT = "col_max_int";
    private static final String ID_COLUMN_STANDARD_DEV = "col_standard_dev";

    // table columns informations
    static final ColumnInfo[] columnInfos = {
            new ColumnInfo("", ID_COLUMN_ICON, "", Image.class, 26, 26, true, false),
            new ColumnInfo("Name", ID_COLUMN_NAME, "ROI name (double click in a cell to edit)", String.class, 60, 100,
                    true, false),
            new ColumnInfo("Type", ID_COLUMN_TYPE, "ROI type", String.class, 60, 80, false, false),
            new ColumnInfo("Position X", ID_COLUMN_POSITION_X, "X Position of the ROI", Double.class, 30, 60, false,
                    false),
            new ColumnInfo("Position Y", ID_COLUMN_POSITION_Y, "Y Position of the ROI", Double.class, 30, 60, false,
                    false),
            new ColumnInfo("Position Z", ID_COLUMN_POSITION_Z, "Z Position of the ROI", Double.class, 30, 60, false,
                    false),
            new ColumnInfo("Position T", ID_COLUMN_POSITION_T, "T Position of the ROI", Double.class, 30, 60, false,
                    false),
            new ColumnInfo("Position C", ID_COLUMN_POSITION_C, "C Position of the ROI", Double.class, 30, 60, false,
                    false),
            new ColumnInfo("Size X", ID_COLUMN_SIZE_X, "X dimension size of the ROI (width)", Double.class, 30, 60,
                    false, false),
            new ColumnInfo("Size Y", ID_COLUMN_SIZE_Y, "Y dimension size of the ROI (heigth)", Double.class, 30, 60,
                    false, false),
            new ColumnInfo("Size Z", ID_COLUMN_SIZE_Z, "Z dimension size of the ROI (depth)", Double.class, 30, 60,
                    false, false),
            new ColumnInfo("Size T", ID_COLUMN_SIZE_T, "T dimension size of the ROI (time)", Double.class, 30, 60,
                    false, false),
            new ColumnInfo("Size C", ID_COLUMN_SIZE_C, "C dimension size of the ROI (channel)", Double.class, 30, 60,
                    false, false),
            new ColumnInfo("Contour", ID_COLUMN_CONTOUR, "Number of points for the contour", Double.class, 30, 60,
                    false, false),
            new ColumnInfo("Interior", ID_COLUMN_POINTS, "Number of points for the interior", Double.class, 30, 60,
                    false, false),
            new ColumnInfo("Perimeter", ID_COLUMN_PERIMETER, "Perimeter", String.class, 40, 80, true, false),
            new ColumnInfo("Area", ID_COLUMN_AREA, "Area", String.class, 40, 80, true, false),
            new ColumnInfo("Surface Area", ID_COLUMN_SURFACE_AREA, "Surface Area", String.class, 40, 80, false, false),
            new ColumnInfo("Volume", ID_COLUMN_VOLUME, "Volume", String.class, 40, 80, false, false),
            new ColumnInfo("Min Intensity", ID_COLUMN_MIN_INT, "Minimum pixel intensity", Double.class, 40, 100, false,
                    true),
            new ColumnInfo("Mean Intensity", ID_COLUMN_MEAN_INT, "Mean pixel intensity", Double.class, 40, 100, false,
                    true),
            new ColumnInfo("Max Intensity", ID_COLUMN_MAX_INT, "Maximum pixel intensity", Double.class, 40, 100, false,
                    true),
            new ColumnInfo("Std Deviation", ID_COLUMN_STANDARD_DEV, "Standard deviation", Double.class, 40, 100, false,
                    true)};

    // GUI
    AbstractTreeTableModel tableModel;
    ListSelectionModel tableSelectionModel;
    JXTreeTable table;
    IcyTextField nameFilter;
    JLabel roiNumberLabel;
    RoiControlPanel roiControlPanel;

    // ROI info list cache
    List<ROIInfo> rois;
    List<ROIInfo> filteredRois;

    // internals
    final XMLPreferences preferences;
    final Semaphore modifySelection;
    // complete refresh of the table
    final Runnable roiListRefresher;
    final Runnable tableDataStructureRefresher;
    final Runnable tableDataRefresher;
    final Runnable tableSelectionRefresher;
    final Thread roiInfoComputer;
    final LinkedBlockingQueue<ROIInfo> roisToCompute;
    int columnCount;

    public RoisPanel()
    {
        super("ROI", "roiPanel", new Point(100, 100), new Dimension(400, 600));

        preferences = GeneralPreferences.getPreferences().node(PREF_ID);
        rois = new ArrayList<ROIInfo>();
        filteredRois = new ArrayList<ROIInfo>();
        modifySelection = new Semaphore(1);
        columnCount = 0;

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

        initialize();

        LookAndFeelUtil.addSkinChangeListener(new SkinChangeListener()
        {
            @Override
            public void skinChanged()
            {
                // fix the row height which is not preserved on skin change
                table.setRowHeight(24);
            }
        });

        // build table model
        tableModel = new AbstractTreeTableModel()
        {
            @Override
            public int getColumnCount()
            {
                return ((TableColumnModelExt) table.getColumnModel()).getColumnCount(true);
            }

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

                if (ci != null)
                {
                    String result = ci.name;

                    if (ci.channelInfo)
                        result += getTableChannelName(getTableChannelIndex(column));

                    return result;
                }

                return "";
            }

            // @Override
            // public int getRowCount()
            // {
            // return filteredRois.size();
            // }
            //
            // @Override
            // public Object getValueAt(int row, int column)
            // {
            // // substance occasionally do not check size before getting value
            // if (row >= filteredRois.size())
            // return null;
            //
            // final ROIInfo roiInfo = filteredRois.get(row);
            // final ROI roi = roiInfo.getROI();
            // final int columnInd = getTableColumnInfoIndex(column);
            // final int channelInd = getTableChannelIndex(column);
            //
            // switch (columnInd)
            // {
            // case 0: // icon
            // return roi.getIcon();
            // case 1: // name
            // return roi.getName();
            // case 2: // type
            // return roi.getSimpleClassName();
            // case 3: // position X
            // return Double.valueOf(roiInfo.getPositionX());
            // case 4: // position Y
            // return Double.valueOf(roiInfo.getPositionY());
            // case 5: // position Z
            // return Double.valueOf(roiInfo.getPositionZ());
            // case 6: // position T
            // return Double.valueOf(roiInfo.getPositionT());
            // case 7: // position C
            // return Double.valueOf(roiInfo.getPositionC());
            // case 8: // size X
            // return Double.valueOf(roiInfo.getSizeX());
            // case 9: // size Y
            // return Double.valueOf(roiInfo.getSizeY());
            // case 10: // size Z
            // return Double.valueOf(roiInfo.getSizeZ());
            // case 11: // size T
            // return Double.valueOf(roiInfo.getSizeT());
            // case 12: // size C
            // return Double.valueOf(roiInfo.getSizeC());
            // case 13: // contour points
            // return Double.valueOf(roiInfo.getNumberOfContourPoints());
            // case 14: // points
            // return Double.valueOf(roiInfo.getNumberOfPoints());
            // case 15: // perimeter
            // return roiInfo.getPerimeter();
            // case 16: // area
            // return roiInfo.getArea();
            // case 17: // surface area
            // return roiInfo.getSurfaceArea();
            // case 18: // volume
            // return roiInfo.getVolume();
            // case 19: // min intensity
            // return Double.valueOf(roiInfo.getMinIntensities(channelInd));
            // case 20: // mean intensity
            // return Double.valueOf(roiInfo.getMeanIntensities(channelInd));
            // case 21: // max intensity
            // return Double.valueOf(roiInfo.getMaxIntensities(channelInd));
            // case 22: // standard deviation
            // return Double.valueOf(roiInfo.getStandardDeviation(channelInd));
            // }
            //
            // return "";
            // }
            //
            // @Override
            // public void setValueAt(Object value, int row, int column)
            // {
            // // substance occasionally do not check size before getting value
            // if (row >= filteredRois.size())
            // return;
            //
            // final ROIInfo roiInfo = filteredRois.get(row);
            // final ROI roi = roiInfo.getROI();
            //
            // switch (column)
            // {
            // case 1: // name
            // roi.setName((String) value);
            // break;
            // }
            // }
            //
            // @Override
            // public boolean isCellEditable(int row, int column)
            // {
            // return (column == 1);
            // }

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

                if (ci != null)
                    return ci.type;

                return String.class;
            }

            @Override
            public Object getValueAt(Object node, int column)
            {
                // TODO Auto-generated method stub
                return null;
            }

            @Override
            public Object getChild(Object parent, int index)
            {

                // TODO Auto-generated method stub
                return null;
            }

            @Override
            public int getChildCount(Object parent)
            {
                // TODO Auto-generated method stub
                return 0;
            }

            @Override
            public int getIndexOfChild(Object parent, Object child)
            {
                // TODO Auto-generated method stub
                return 0;
            }
        };

        // set table model
        table.setTreeTableModel(tableModel);
        // modify column properties
        buildTableColumns();
        // alternate highlight
        table.setHighlighters(HighlighterFactory.createSimpleStriping());
        // disable extra actions from column control
        ((ColumnControlButton) table.getColumnControl()).setAdditionalActionsVisible(false);

        // set selection model
        tableSelectionModel = table.getSelectionModel();
        tableSelectionModel.addListSelectionListener(this);
        tableSelectionModel.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);

        roisToCompute = new LinkedBlockingQueue<ROIInfo>();
        roiInfoComputer = new Thread(this, "ROI properties calculator");
        roiInfoComputer.setPriority(Thread.MIN_PRIORITY);
        roiInfoComputer.start();

        // set shortcuts
        buildActionMap();

        refreshRois();
    }

    private void initialize()
    {
        // need filter before load
        nameFilter = new IcyTextField();
        nameFilter.setToolTipText("Enter a string sequence to filter ROI on name");
        nameFilter.addTextChangeListener(this);

        roiNumberLabel = new JLabel("No ROI");

        // build table
        table = new JXTreeTable();
        table.setAutoStartEditOnKeyStroke(false);
        table.setRowHeight(24);
        table.setShowVerticalLines(false);
        table.setColumnControlVisible(true);
        table.setColumnSelectionAllowed(false);
        table.setRowSelectionAllowed(true);
        table.setAutoCreateRowSorter(false);
        table.setAutoCreateColumnsFromModel(false);

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

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

        // build control panel
        roiControlPanel = new RoiControlPanel(this);

        setLayout(new BorderLayout());
        add(GuiUtil.createLineBoxPanel(nameFilter, Box.createHorizontalStrut(8), roiNumberLabel,
                Box.createHorizontalStrut(4)), BorderLayout.NORTH);
        add(middlePanel, BorderLayout.CENTER);
        add(roiControlPanel, BorderLayout.SOUTH);

        validate();
    }

    void buildActionMap()
    {
        final InputMap imap = table.getInputMap(JComponent.WHEN_FOCUSED);
        final ActionMap amap = table.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);
    }

    boolean buildTableColumns()
    {
        final int newColCount = getTableColumnCount();

        // nothing to change
        if (columnCount == newColCount)
            return false;
        final TableColumnModelExt colModel = (TableColumnModelExt) table.getColumnModel();
        final List<TableColumn> columns = colModel.getColumns(true);

        // remove row sorter the time we update columns
        table.setRowSorter(null);

        // TODO: try to find a way to disable table refresh while modifying column

        // and regenerate them
        for (int i = 0; i < newColCount; i++)
        {
            final ColumnInfo ci = getTableColumnInfo(i);

            // can't retrieve column informations --> pass to the next
            if (ci == null)
                continue;

            final TableColumnExt col;

            if (i >= columns.size())
                col = new TableColumnExt(i);
            else
                col = (TableColumnExt) columns.get(i);

            // build column name & tool tip
            String name = ci.name;
            String toolTip = ci.toolTip;

            if (ci.channelInfo)
            {
                name += getTableChannelName(getTableChannelIndex(i));
                toolTip += getTableChannelName(getTableChannelIndex(i));
            }

            // column changed ?
            if ((!name.equals(col.getHeaderValue())) || (!ci.id.equals(col.getIdentifier())))
            {
                col.setIdentifier(ci.id);
                col.setMinWidth(ci.minSize);
                col.setPreferredWidth(ci.preferredSize);
                col.setHeaderValue(name);
                col.setToolTipText(toolTip);
                col.setVisible(preferences.getBoolean(ci.id, ci.defVisible));
                col.setModelIndex(i);

                // special icon index
                if (i == 0)
                {
                    col.setMaxWidth(ci.preferredSize);
                    col.setCellRenderer(new ImageTableCellRenderer(ci.preferredSize - 2));
                    col.setResizable(false);
                }

                // need to add this column ?
                if (i >= columns.size())
                {
                    col.addPropertyChangeListener(this);
                    // add the column
                    colModel.addColumn(col);
                }
            }
        }

        // remove old columns no more in use
        for (int i = newColCount; i < columns.size(); i++)
        {
            final TableColumn col = columns.get(i);

            // remove listener
            col.removePropertyChangeListener(this);
            // then remove column
            colModel.removeColumn(col);
        }

        // set row sorter back
        // table.setRowSorter(new TableSortController<TableModel>(tableModel));
        // and store new number of column
        columnCount = newColCount;

        return true;
    }

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

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

        return 1;
    }

    /**
     * Returns table column suffix for the specified channel
     */
    String getTableChannelName(int ind)
    {
        final Sequence sequence = getSequence();

        if ((sequence != null) && (ind < sequence.getSizeC()))
            return " (" + sequence.getChannelName(ind) + ")";

        return "";
    }

    /**
     * Returns the minimum width for the specified column index
     */
    public int getMinColumnWidth(int ind)
    {
        return getDefaultColumnWidth(ind) / 2;
    }

    /**
     * Returns the default width for the specified column index
     */
    public int getDefaultColumnWidth(int ind)
    {
//        final Class<?> type = getColumnType(ind);
//
//        if (type == Integer.class)
//            return 60;
//        if (type == Double.class)
//            return 80;
//        if (type == String.class)
//            return 100;

        return 80;
    }

    /**
     * Get number of column in the table.
     */
    int getTableColumnCount()
    {
        final int channelCnt = getChannelCount();

        int res = 0;
        for (int i = 0; i < columnInfos.length; i++)
        {
            final ColumnInfo ci = columnInfos[i];

            if (ci.channelInfo)
                res += channelCnt;
            else
                res++;
        }

        return res;
    }

    /**
     * Get column info index for specified column index.
     */
    int getTableColumnInfoIndex(int index)
    {
        final int channelCnt = getChannelCount();

        int ind = 0;
        for (int i = 0; i < columnInfos.length; i++)
        {
            final ColumnInfo ci = columnInfos[i];

            if (ind == index)
                return i;

            if (ci.channelInfo)
            {
                ind += channelCnt;
                if (ind > index)
                    return i;
            }
            else
                ind++;
        }

        return -1;
    }

    /**
     * Get column info for specified column index.
     */
    ColumnInfo getTableColumnInfo(int index)
    {
        final int ind = getTableColumnInfoIndex(index);

        if (ind != -1)
            return columnInfos[ind];

        return null;
    }

    /**
     * Get the channel index represented by the specified column index.
     */
    int getTableChannelIndex(int index)
    {
        final int channelCnt = getChannelCount();

        int ind = 0;
        for (int i = 0; i < columnInfos.length; i++)
        {
            final ColumnInfo ci = columnInfos[i];

            if (ind == index)
                return 0;

            if (ci.channelInfo)
            {
                ind += channelCnt;
                if (ind > index)
                    return channelCnt - (ind - index);
            }
            else
                ind++;
        }

        return 0;
    }

    private XMLPreferences getPreferences()
    {
        return GeneralPreferences.getPreferences().node(PREF_ID);
    }

    Sequence getSequence()
    {
        return Icy.getMainInterface().getActiveSequence();
    }

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

    ROIInfo getRoiInfoToCompute()
    {
        return null;
    }

    @Override
    public void run()
    {
        while (true)
        {
            try
            {
                roisToCompute.take().compute();
            }
            catch (InterruptedException e)
            {
                // ignore
            }
        }
    }

    /**
     * Return index of specified ROI in the ROI list
     */
    private int getRoiIndex(ROI roi)
    {
        for (int i = 0; i < filteredRois.size(); i++)
            if (filteredRois.get(i).getROI() == roi)
                return i;

        return -1;
    }

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

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

        if (ind == -1)
            return ind;

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

    /**
     * Returns the visible ROI in the ROI control panel.
     */
    public List<ROI> getVisibleRois()
    {
        final List<ROIInfo> roisInfo = filteredRois;
        final List<ROI> result = new ArrayList<ROI>(roisInfo.size());

        for (ROIInfo roiInfo : roisInfo)
            result.add(roiInfo.getROI());

        return result;
    }

    /**
     * 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 selected ROI in the ROI control panel.
     */
    public List<ROIInfo> getSelectedRoisInfo()
    {
        final List<ROIInfo> result = new ArrayList<ROIInfo>(table.getRowCount());

        for (int rowIndex : table.getSelectedRows())
        {
            int index = -1;

            if (rowIndex != -1)
            {
                try
                {
                    index = table.convertRowIndexToModel(rowIndex);
                }
                catch (IndexOutOfBoundsException e)
                {
                    // ignore
                }
            }

            if ((index >= 0) && (index < filteredRois.size()))
                result.add(filteredRois.get(index));
        }

        return result;
    }

    /**
     * Get the selected ROI in the ROI control panel.<br>
     * This actually returns selected ROI from the ROI table in ROI panel (cached).
     */
    public List<ROI> getSelectedRois()
    {
        final List<ROIInfo> roisInfo = getSelectedRoisInfo();
        final List<ROI> result = new ArrayList<ROI>(roisInfo.size());

        for (ROIInfo roiInfo : roisInfo)
            result.add(roiInfo.getROI());

        return result;
    }

    /**
     * Select the specified list of ROI in the ROI Table
     */
    protected void setSelectedRoisInternal(HashSet<ROI> newSelected)
    {
        final List<ROIInfo> modelRois = filteredRois;

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

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

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

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

                    if (ind > -1)
                        tableSelectionModel.addSelectionInterval(ind, ind);
                }
            }
        }
        finally
        {
            // end selection change
            tableSelectionModel.setValueIsAdjusting(false);
        }
    }

    protected List<ROIInfo> getFilteredList(String filter)
    {
        final List<ROIInfo> result = new ArrayList<ROIInfo>();

        // no need to synchronize on 'rois' as it can be only modified prior to this call
        if (StringUtil.isEmpty(filter, true))
            result.addAll(rois);
        else
        {
            final String text = filter.trim().toLowerCase();

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

        return result;
    }

    void refreshRoiNumber()
    {
        final int rowCount = table.getRowCount();

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

    /**
     * refresh ROI list
     */
    protected void refreshRois()
    {
        ThreadUtil.runSingle(roiListRefresher);
    }

    /**
     * refresh ROI list
     */
    protected void refreshRoisInternal()
    {
        final Sequence sequence = getSequence();

        if (sequence != null)
        {
            final List<ROI> newRois = sequence.getROIs();
            final List<ROI> oldRois = new ArrayList<ROI>();

            // build old ROI list
            for (int i = 0; i < rois.size(); i++)
                oldRois.add(rois.get(i).getROI());

            // remove ROI which are no more in the list (use HashSet for fast contains())
            final Set<ROI> newRoiSet = new HashSet<ROI>(newRois);
            for (int i = oldRois.size() - 1; i >= 0; i--)
                if (!newRoiSet.contains(oldRois.get(i)))
                    roisToCompute.remove(rois.remove(i));

            // add ROI which has been added (use HashSet for fast contains())
            final Set<ROI> oldRoiSet = new HashSet<ROI>(oldRois);
            for (ROI roi : newRois)
                if (!oldRoiSet.contains(roi))
                    rois.add(new ROIInfo(roi));
        }
        else
        {
            // no change --> exit
            if (rois.isEmpty())
                return;

            // clear ROI list
            rois.clear();
        }

        // need to refresh the filtered list
        final List<ROIInfo> newFilteredRois = getFilteredList(nameFilter.getText());
        final int newSize = newFilteredRois.size();
        final int oldSize = filteredRois.size();

        // easy optimization
        if ((newSize == 0) && (oldSize == 0))
            return;

        // same size
        if (newSize == oldSize)
        {
            // same values, don't need to update it
            if (new HashSet<ROIInfo>(newFilteredRois).containsAll(filteredRois))
                return;
        }

        // update filtered ROI list
        filteredRois = newFilteredRois;
        // refresh whole table
        refreshTableDataStructure();
    }

    public void refreshTableDataStructure()
    {
        ThreadUtil.runSingle(tableDataStructureRefresher);
    }

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

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

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

        ThreadUtil.invokeNow(new Runnable()
        {
            @Override
            public void run()
            {
                modifySelection.acquireUninterruptibly();
                try
                {
                    // rebuild columns
                    if (!buildTableColumns())
                        // notify table data changed
                        // tableModel.fireTableDataChanged();
                        ;
                    // selection to restore ?
                    if (!newSelectedRois.isEmpty())
                        setSelectedRoisInternal(newSelectedRois);

                    refreshRoiNumber();
                }
                catch (Exception e)
                {
                    e.printStackTrace();
                }
                finally
                {
                    modifySelection.release();
                }
            }
        });

        // notify the ROI control panel that selection changed
        roiControlPanel.selectionChanged();
    }

    public void refreshTableData()
    {
        ThreadUtil.runSingle(tableDataRefresher);
    }

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

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

                    // we use RowsUpdated event to keep selection (DataChanged remove selection)
                    // if (rowCount > 0)
                    // tableModel.fireTableRowsUpdated(0, rowCount - 1);

                    refreshRoiNumber();
                }
                catch (Exception e)
                {
                    // ignore possible exception here
                }
            }

        });

        // notify the ROI control panel that selection changed (force data refresh)
        roiControlPanel.selectionChanged();
    }

    public void refreshTableSelection()
    {
        ThreadUtil.runSingle(tableSelectionRefresher);
    }

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

        final HashSet<ROI> newSelectedRois;
        final List<ROI> currentSelectedRois = getSelectedRois();
        final Sequence sequence = getSequence();

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

        // selection changed ?
        if (!CollectionUtil.equals(currentSelectedRois, newSelectedRois))
        {
            ThreadUtil.invokeNow(new Runnable()
            {
                @Override
                public void run()
                {
                    modifySelection.acquireUninterruptibly();
                    tableSelectionModel.setValueIsAdjusting(true);
                    try
                    {
                        // set new selection
                        setSelectedRoisInternal(newSelectedRois);
                    }
                    catch (Exception e)
                    {
                        e.printStackTrace();
                    }
                    finally
                    {
                        tableSelectionModel.setValueIsAdjusting(false);
                        // important to release it after the valueIsAdjusting
                        modifySelection.release();
                    }
                }
            });

            // notify the ROI control panel that selection changed
            roiControlPanel.selectionChanged();
        }
    }

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

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

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

        // column name
        for (int j = 1; j < numcols; j++)
        {
            sbf.append(table.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 = table.getModel().getValueAt(table.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()
    {
        // Check to ensure we have selected only a contiguous block of cells
        final int numcols = tableModel.getColumnCount();
        // final int numrows = tableModel.getRowCount();
        final int numrows = tableModel.getChildCount(tableModel.getRoot());

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

        final StringBuffer sbf = new StringBuffer();

        // column name
        for (int j = 1; j < numcols; j++)
        {
            sbf.append(tableModel.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 = tableModel.getValueAt(i, j);

                sbf.append(value);

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

        return sbf.toString();
    }

    @Override
    public void propertyChange(PropertyChangeEvent evt)
    {
        // column visibility changed ?
        if ("visible".equals(evt.getPropertyName()))
        {
            // store column visibility in preferences
            final TableColumnExt column = (TableColumnExt) evt.getSource();
            getPreferences().putBoolean((String) column.getIdentifier(), column.isVisible());
        }
    }

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

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

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

            // update selected ROI in sequence
            if (sequence != null)
                sequence.setSelectedROIs(selectedRois);

            // notify the ROI control panel that selection changed
            roiControlPanel.selectionChanged();
        }
        finally
        {
            modifySelection.release();
        }
    }

    @Override
    public void sequenceActivated(Sequence value)
    {
        // force column rebuild
        columnCount = 0;
        // 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:
                final SequenceEventType type = event.getType();

                // changed event already handled by ROIInfo
                if ((type == SequenceEventType.ADDED) || (type == SequenceEventType.REMOVED))
                    // refresh the ROI list
                    refreshRois();
                break;

            case SEQUENCE_DATA:
                // notify ROI info that sequence data changed
                for (ROIInfo roiInfo : filteredRois)
                    roiInfo.sequenceDataChanged();

                // if data changed (more Z or T) we need to refresh action
                // so we can change ROI position correctly
                roiControlPanel.refreshROIActions();
                break;

            case SEQUENCE_TYPE:
                // if type changed (number of channel) we need to refresh action
                // so we change change ROI position correctly
                roiControlPanel.refreshROIActions();
                break;
        }
    }

    public class ROIInfo implements ROIListener
    {
        private ROI roi;
        private double[] standardDeviation;
        private IntensityInfo[] intensityInfos;

        // cached
        private double numberContourPoints;
        private double numberPoints;
        private boolean sequenceInfInvalid;
        private boolean roiInfInvalid;

        public ROIInfo(ROI roi)
        {
            this.roi = roi;

            numberContourPoints = 0d;
            numberPoints = 0d;
            standardDeviation = new double[0];
            intensityInfos = new IntensityInfo[0];
            sequenceInfInvalid = true;
            roiInfInvalid = true;

            requestCompute();

            roi.addListener(this);
        }

        public void dispose()
        {
            roi.removeListener(this);
            roi = null;
        }

        /**
         * Recompute ROI informations
         */
        public void compute()
        {
            try
            {
                if (roiInfInvalid)
                {
                    // refresh points number calculation
                    numberContourPoints = MathUtil.roundSignificant(roi.getNumberOfContourPoints(), 5, true);
                    numberPoints = MathUtil.roundSignificant(roi.getNumberOfPoints(), 5, true);

                    roiInfInvalid = false;
                }

                if (sequenceInfInvalid)
                {
                    final Sequence sequence = getSequence();

                    if (sequence != null)
                    {
                        // calculate intensity infos
                        final Rectangle5D roiBounds = roi.getBounds5D();
                        final int sizeC = sequence.getSizeC();

                        final double[] sd = new double[sizeC];
                        final IntensityInfo[] iis = new IntensityInfo[sizeC];

                        for (int c = 0; c < sizeC; c++)
                        {
                            iis[c] = new IntensityInfo();
                            sd[c] = 0d;

                            if ((c >= roiBounds.getMinC()) && (c < roiBounds.getMaxC()))
                            {
                                final IntensityInfo ii = ROIUtil.getIntensityInfo(sequence, roi, -1, -1, c);

                                if (ii != null)
                                {
                                    // round values
                                    ii.minIntensity = MathUtil.roundSignificant(ii.minIntensity, 5, true);
                                    ii.meanIntensity = MathUtil.roundSignificant(ii.meanIntensity, 5, true);
                                    ii.maxIntensity = MathUtil.roundSignificant(ii.maxIntensity, 5, true);

                                    iis[c] = ii;
                                }

                                sd[c] = MathUtil.roundSignificant(
                                        ROIUtil.getStandardDeviation(sequence, roi, -1, -1, c), 5, true);
                            }
                        }

                        intensityInfos = iis;
                        standardDeviation = sd;
                    }
                    else
                    {
                        intensityInfos = new IntensityInfo[0];
                        standardDeviation = new double[0];
                    }

                    sequenceInfInvalid = false;
                }
            }
            catch (Throwable e)
            {
                // we can have some exception here as this is an asynch process (just ignore)
                if (e instanceof OutOfMemoryError)
                    System.err.println("Cannot compute ROI infos: Not enought memory !");
            }

            refreshTableData();
        }

        void requestCompute()
        {
            if (!roisToCompute.contains(this))
            {
                try
                {
                    roisToCompute.put(this);
                }
                catch (InterruptedException e)
                {
                    // ignore
                }
            }
        }

        public ROI getROI()
        {
            return roi;
        }

        public String getName()
        {
            return roi.getName();
        }

        public boolean isRoiInfOutdated()
        {
            return roiInfInvalid;
        }

        public boolean isSequenceInfOutdated()
        {
            return sequenceInfInvalid;
        }

        public double getNumberOfContourPoints()
        {
            // need to recompute
            if (roiInfInvalid)
                requestCompute();

            return numberContourPoints;
        }

        public double getNumberOfPoints()
        {
            // need to recompute
            if (roiInfInvalid)
                requestCompute();

            return numberPoints;
        }

        public String getPerimeter()
        {
            final Sequence seq = getSequence();
            if (seq == null)
                return "";

            return ROIUtil.getContourSize(seq, getNumberOfContourPoints(), roi, 2, 5);
        }

        public String getArea()
        {
            final Sequence seq = getSequence();
            if (seq == null)
                return "";

            return ROIUtil.getInteriorSize(seq, getNumberOfPoints(), roi, 2, 5);
        }

        public String getSurfaceArea()
        {
            final Sequence seq = getSequence();
            if (seq == null)
                return "";

            return ROIUtil.getContourSize(seq, getNumberOfContourPoints(), roi, 3, 5);
        }

        public String getVolume()
        {
            final Sequence seq = getSequence();
            if (seq == null)
                return "";

            return ROIUtil.getInteriorSize(seq, getNumberOfPoints(), roi, 3, 5);
        }

        public double getMinIntensities(int channel)
        {
            // need to recompute
            if (sequenceInfInvalid)
                requestCompute();

            final IntensityInfo[] infos = intensityInfos;
            if (channel < infos.length)
                return infos[channel].minIntensity;

            return 0d;
        }

        public double getMeanIntensities(int channel)
        {
            // need to recompute
            if (sequenceInfInvalid)
                requestCompute();

            final IntensityInfo[] infos = intensityInfos;
            if (channel < infos.length)
                return infos[channel].meanIntensity;

            return 0d;
        }

        public double getMaxIntensities(int channel)
        {
            // need to recompute
            if (sequenceInfInvalid)
                requestCompute();

            final IntensityInfo[] infos = intensityInfos;
            if (channel < infos.length)
                return infos[channel].maxIntensity;

            return 0d;
        }

        public double getStandardDeviation(int channel)
        {
            // need to recompute
            if (sequenceInfInvalid)
                requestCompute();

            final double[] stdDev = standardDeviation;
            if (channel < stdDev.length)
                return stdDev[channel];

            return 0d;
        }

        // public IntensityInfo[] getIntensities()
        // {
        // // need to recompute
        // if (sequenceInfInvalid)
        // requestCompute();
        //
        // return intensityInfos;
        // }

        public double getPositionX()
        {
            final Rectangle5D bounds = roi.getBounds5D();

            // special case of infinite dimension
            if (bounds.getSizeX() == Double.POSITIVE_INFINITY)
                return -1d;

            return MathUtil.roundSignificant(bounds.getX(), 5, true);
        }

        public double getPositionY()
        {
            final Rectangle5D bounds = roi.getBounds5D();

            // special case of infinite dimension
            if (bounds.getSizeY() == Double.POSITIVE_INFINITY)
                return -1d;

            return MathUtil.roundSignificant(bounds.getY(), 5, true);
        }

        public double getPositionZ()
        {
            final Rectangle5D bounds = roi.getBounds5D();

            // special case of infinite dimension
            if (bounds.getSizeZ() == Double.POSITIVE_INFINITY)
                return -1d;

            return MathUtil.roundSignificant(bounds.getZ(), 5, true);
        }

        public double getPositionT()
        {
            final Rectangle5D bounds = roi.getBounds5D();

            // special case of infinite dimension
            if (bounds.getSizeT() == Double.POSITIVE_INFINITY)
                return -1d;

            return MathUtil.roundSignificant(bounds.getT(), 5, true);
        }

        public double getPositionC()
        {
            final Rectangle5D bounds = roi.getBounds5D();

            // special case of infinite dimension
            if (bounds.getSizeC() == Double.POSITIVE_INFINITY)
                return -1d;

            return MathUtil.roundSignificant(bounds.getC(), 5, true);
        }

        public double getSizeX()
        {
            final double v = roi.getBounds5D().getSizeX();

            // special case of infinite dimension
            if (v == Double.POSITIVE_INFINITY)
                return v;

            return MathUtil.roundSignificant(v, 5, true);
        }

        public double getSizeY()
        {
            final double v = roi.getBounds5D().getSizeY();

            // special case of infinite dimension
            if (v == Double.POSITIVE_INFINITY)
                return v;

            return MathUtil.roundSignificant(v, 5, true);
        }

        public double getSizeZ()
        {
            final double v = roi.getBounds5D().getSizeZ();

            // special case of infinite dimension
            if (v == Double.POSITIVE_INFINITY)
                return v;

            return MathUtil.roundSignificant(v, 5, true);
        }

        public double getSizeT()
        {
            final double v = roi.getBounds5D().getSizeT();

            // special case of infinite dimension
            if (v == Double.POSITIVE_INFINITY)
                return v;

            return MathUtil.roundSignificant(v, 5, true);
        }

        public double getSizeC()
        {
            final double v = roi.getBounds5D().getSizeC();

            // special case of infinite dimension
            if (v == Double.POSITIVE_INFINITY)
                return v;

            return MathUtil.roundSignificant(v, 5, true);
        }

        public String getPositionXAsString()
        {
            final Rectangle5D bounds = roi.getBounds5D();

            // special case of infinite dimension
            if (bounds.getSizeX() == Double.POSITIVE_INFINITY)
                return "all";

            return StringUtil.toString(MathUtil.roundSignificant(bounds.getX(), 5, true));
        }

        public String getPositionYAsString()
        {
            final Rectangle5D bounds = roi.getBounds5D();

            // special case of infinite dimension
            if (bounds.getSizeY() == Double.POSITIVE_INFINITY)
                return "all";

            return StringUtil.toString(MathUtil.roundSignificant(bounds.getY(), 5, true));
        }

        public String getPositionZAsString()
        {
            final Rectangle5D bounds = roi.getBounds5D();

            // special case of infinite dimension
            if (bounds.getSizeZ() == Double.POSITIVE_INFINITY)
                return "all";

            return StringUtil.toString(MathUtil.roundSignificant(bounds.getZ(), 5, true));
        }

        public String getPositionTAsString()
        {
            final Rectangle5D bounds = roi.getBounds5D();

            // special case of infinite dimension
            if (bounds.getSizeT() == Double.POSITIVE_INFINITY)
                return "all";

            return StringUtil.toString(MathUtil.roundSignificant(bounds.getT(), 5, true));
        }

        public String getPositionCAsString()
        {
            final Rectangle5D bounds = roi.getBounds5D();

            // special case of infinite dimension
            if (bounds.getSizeC() == Double.POSITIVE_INFINITY)
                return "all";

            return StringUtil.toString(MathUtil.roundSignificant(bounds.getC(), 5, true));
        }

        public String getSizeXAsString()
        {
            final double v = roi.getBounds5D().getSizeX();

            // special case of infinite dimension
            if (v == Double.POSITIVE_INFINITY)
                return MathUtil.INFINITE_STRING;

            return StringUtil.toString(MathUtil.roundSignificant(v, 5, true));
        }

        public String getSizeYAsString()
        {
            final double v = roi.getBounds5D().getSizeY();

            // special case of infinite dimension
            if (v == Double.POSITIVE_INFINITY)
                return MathUtil.INFINITE_STRING;

            return StringUtil.toString(MathUtil.roundSignificant(v, 5, true));
        }

        public String getSizeZAsString()
        {
            final double v = roi.getBounds5D().getSizeZ();

            // special case of infinite dimension
            if (v == Double.POSITIVE_INFINITY)
                return MathUtil.INFINITE_STRING;

            return StringUtil.toString(MathUtil.roundSignificant(v, 5, true));
        }

        public String getSizeTAsString()
        {
            final double v = roi.getBounds5D().getSizeT();

            // special case of infinite dimension
            if (v == Double.POSITIVE_INFINITY)
                return MathUtil.INFINITE_STRING;

            return StringUtil.toString(MathUtil.roundSignificant(v, 5, true));
        }

        public String getSizeCAsString()
        {
            final double v = roi.getBounds5D().getSizeC();

            // special case of infinite dimension
            if (v == Double.POSITIVE_INFINITY)
                return MathUtil.INFINITE_STRING;

            return StringUtil.toString(MathUtil.roundSignificant(v, 5, true));
        }

        @Override
        public void roiChanged(ROIEvent event)
        {
            switch (event.getType())
            {
                default:
                    // ROI selected ? --> propagate event control panel
                    if (roi.isSelected())
                        roiControlPanel.roiChanged(event);
                    break;

                case ROI_CHANGED:
                    // notify control panel that ROI changed
                    if (roi.isSelected())
                        roiControlPanel.roiChanged(event);

                    sequenceInfInvalid = true;
                    roiInfInvalid = true;
                    requestCompute();
                    break;

                case SELECTION_CHANGED:
                    // update ROI selection
                    refreshTableSelection();
                    break;

                case PROPERTY_CHANGED:
                    final String property = event.getPropertyName();

                    if (ROI.PROPERTY_NAME.equals(property) || ROI.PROPERTY_ICON.equals(property))
                        refreshTableData();
                    break;
            }
        }

        public void sequenceDataChanged()
        {
            sequenceInfInvalid = true;
            requestCompute();
        }

        @Override
        public boolean equals(Object obj)
        {
            // consider same ROIInfo if the inner ROI is the same
            if (obj instanceof ROIInfo)
                return ((ROIInfo) obj).getROI() == getROI();

            return super.equals(obj);
        }
    }

    public static class ColumnInfo
    {
        final String id;
        final String name;
        final String toolTip;
        final Class<?> type;
        final int minSize;
        final int preferredSize;
        final boolean defVisible;
        final boolean channelInfo;

        public ColumnInfo(String name, String id, String toolTip, Class<?> type, int minSize, int preferredSize,
                boolean defVisible, boolean channelInfo)
        {
            super();

            this.name = name;
            this.id = id;
            this.toolTip = toolTip;
            this.type = type;
            this.minSize = minSize;
            this.preferredSize = preferredSize;
            this.defVisible = defVisible;
            this.channelInfo = channelInfo;
        }

        public ColumnInfo(ColumnInfo info)
        {
            this(info.name, info.id, info.toolTip, info.type, info.minSize, info.preferredSize, info.defVisible,
                    info.channelInfo);
        }

    }
}