/*
* 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;
// }
// }
}