/*
*------------------------------------------------------------------------------
* Copyright (C) 2006-2015 University of Dundee. All rights reserved.
*
*
* This program 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 2 of the License, or
* (at your option) any later version.
* This program 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 this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*
*------------------------------------------------------------------------------
*/
package org.openmicroscopy.shoola.agents.measurement.view;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Point;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.File;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import javax.swing.AbstractAction;
import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.Icon;
import javax.swing.JButton;
import javax.swing.JPanel;
import javax.swing.JSlider;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import org.apache.commons.collections.CollectionUtils;
import org.jhotdraw.draw.Figure;
import org.openmicroscopy.shoola.agents.events.measurement.SelectPlane;
import org.openmicroscopy.shoola.agents.measurement.IconManager;
import org.openmicroscopy.shoola.agents.measurement.MeasurementAgent;
import org.openmicroscopy.shoola.agents.measurement.util.TabPaneInterface;
import org.openmicroscopy.shoola.agents.measurement.util.model.AnalysisStatsWrapper;
import org.openmicroscopy.shoola.agents.measurement.util.model.AnalysisStatsWrapper.StatsType;
import org.openmicroscopy.shoola.agents.util.EditorUtil;
import omero.log.Logger;
import org.openmicroscopy.shoola.env.rnd.roi.ROIShapeStatsSimple;
import org.openmicroscopy.shoola.env.ui.UserNotifier;
import org.openmicroscopy.shoola.util.roi.figures.MeasureBezierFigure;
import org.openmicroscopy.shoola.util.roi.figures.MeasureLineFigure;
import org.openmicroscopy.shoola.util.roi.figures.MeasureTextFigure;
import org.openmicroscopy.shoola.util.roi.figures.ROIFigure;
import org.openmicroscopy.shoola.util.roi.model.ROIShape;
import org.openmicroscopy.shoola.util.roi.model.util.Coord3D;
import org.openmicroscopy.shoola.util.roi.model.util.MeasurementUnits;
import org.openmicroscopy.shoola.util.ui.UIUtilities;
import org.openmicroscopy.shoola.util.ui.graphutils.HistogramPlot;
import org.openmicroscopy.shoola.util.ui.graphutils.LinePlot;
import org.openmicroscopy.shoola.util.ui.slider.OneKnobSlider;
import omero.gateway.model.ChannelData;
/**
* Displays the intensities as a graph.
*
* @author Jean-Marie Burel
* <a href="mailto:j.burel@dundee.ac.uk">j.burel@dundee.ac.uk</a>
* @author Donald MacDonald
* <a href="mailto:donald@lifesci.dundee.ac.uk">donald@lifesci.dundee.ac.uk</a>
* @version 3.0
* @since OME3.0
*/
public class GraphPane
extends JPanel
implements TabPaneInterface, PropertyChangeListener, ChangeListener
{
/** Ready state. */
final static int READY = 1;
/** Analyzing state. */
final static int ANALYSING = 0;
/** Index to identify tab */
public final static int INDEX = MeasurementViewerUI.GRAPH_INDEX;
/** The name of the panel. */
private static final String NAME = "Graph Pane";
/** The default color for a line.*/
private static final Color DEFAULT_COLOR = Color.LIGHT_GRAY;
/** Reference to the model. */
private MeasurementViewerModel model;
/** Reference to the controller. */
private MeasurementViewerControl controller;
/** The map of <ROIShape, ROIStats> .*/
private Map ROIStats;
/** The slider controlling the movement of the analysis through Z. */
private OneKnobSlider zSlider;
/** The slider controlling the movement of the analysis through T. */
private OneKnobSlider tSlider;
/** The main panel holding the graphs. */
private JPanel mainPanel;
/** The map of the shape statistics to coordinates. */
private Map<Coord3D, Map<StatsType, Map>> shapeStatsList;
/** Map of the pixel intensity values to coordinates. */
private Map<Coord3D, Map<Integer, ROIShapeStatsSimple>> pixelStats;
/** Map of the coordinates to a shape. */
private Map<Coord3D, ROIShape> shapeMap;
/** List of channel Names. */
private List<String> channelName;
/** List of channel colors. */
private List<Color> channelColour;
/** The current coordinates of the ROI being depicted in the slider. */
private Coord3D coord;
/** The line profile charts. */
private LinePlot lineProfileChart;
/** The histogram chart. */
private HistogramPlot histogramChart;
/** The state of the Graph pane. */
private int state = READY;
/** Reference to the view.*/
private MeasurementViewerUI view;
/** Current shape. */
private ROIShape shape;
/** Button to save the graph as JPEG or PNG.*/
private JButton export;
/**
* Implemented as specified by the I/F {@link TabPaneInterface}
* @see TabPaneInterface#getIndex()
*/
public int getIndex() { return INDEX; }
/**
* Returns <code>true</code> if the figure contained in the ROIShape
* is a line or bezier path, <code>false</code> otherwise.
*
* @param shape The ROIShape containing figure.
* @return See above.
*/
private boolean lineProfileFigure(ROIShape shape)
{
ROIFigure f = shape.getFigure();
if (f instanceof MeasureLineFigure) return true;
if (f instanceof MeasureBezierFigure )
{
MeasureBezierFigure fig = (MeasureBezierFigure) f;
if (!fig.isClosed()) return true;
}
return false;
}
/**
* Finds the minimum value from the channelMin map.
*
* @return See above.
*/
private double channelMinValue()
{
Map channels = model.getActiveChannels();
Entry entry;
Iterator i = channels.entrySet().iterator();
double value = Double.MAX_VALUE;
int channel;
while (i.hasNext())
{
entry = (Entry) i.next();
channel = (Integer) entry.getKey();
value = Math.min(value, model.getMetadata(channel).getGlobalMin());
}
return value;
}
/**
* Finds the maximum value from the channelMin map.
*
* @return See above.
*/
private double channelMaxValue()
{
Map channels = model.getActiveChannels();
Entry entry;
Iterator i = channels.entrySet().iterator();
double value = Double.MIN_VALUE;
int channel;
while (i.hasNext())
{
entry = (Entry) i.next();
channel = (Integer) entry.getKey();
value = Math.max(value, model.getMetadata(channel).getGlobalMax());
}
return value;
}
/** The slider has changed value and the mouse button released. */
private void handleSliderReleased() {
int newZ = zSlider.getValue() - 1;
int newT = tSlider.getValue() - 1;
if (checkPlane(newZ, newT)) {
SelectPlane evt = new SelectPlane(model.getPixelsID(),
zSlider.getValue() - 1, tSlider.getValue() - 1);
MeasurementAgent.getRegistry().getEventBus().post(evt);
}
}
/**
* Controls if the specified coordinates are valid.
* Returns <code>true</code> if the passed values are in the correct ranges,
* <code>false</code> otherwise.
*
* @param z The z coordinate. Must be in the range <code>[0, sizeZ)</code>.
* @param t The t coordinate. Must be in the range <code>[0, sizeT)</code>.
* @return See above.
*/
private boolean checkPlane(int z, int t)
{
if (z < 0 || model.getNumZSections() <= z) return false;
if (t < 0 || model.getNumTimePoints() <= t) return false;
return true;
}
/**
* Saves the graph as JPEG or PNG.
*
* @param file The file where to save the graph
* @param type The format to save into.
*/
public void saveGraph(File file, int type)
{
try {
if (lineProfileChart != null) {
lineProfileChart.saveAs(file, type);
} else {
histogramChart.saveAs(file, type);
}
} catch (Exception e) {
Logger logger = MeasurementAgent.getRegistry().getLogger();
logger.error(this, "Cannot save the graph: "+e.toString());
UserNotifier un = MeasurementAgent.getRegistry().getUserNotifier();
un.notifyInfo("Save Results", "An error occurred while saving " +
"the graph.\nPlease try again.");
}
}
/** Initializes the component composing the display. */
private void initComponents()
{
export = new JButton(controller.getAction(MeasurementViewerControl.EXPORT_GRAPH));
zSlider = new OneKnobSlider();
zSlider.setOrientation(JSlider.VERTICAL);
zSlider.setPaintTicks(false);
zSlider.setPaintLabels(false);
zSlider.setMajorTickSpacing(1);
zSlider.setShowArrows(true);
zSlider.setVisible(false);
zSlider.setEndLabel("Z");
zSlider.setShowEndLabel(true);
tSlider = new OneKnobSlider();
tSlider.setPaintTicks(false);
tSlider.setPaintLabels(false);
tSlider.setMajorTickSpacing(1);
tSlider.setSnapToTicks(true);
tSlider.setShowArrows(true);
tSlider.setVisible(false);
tSlider.setEndLabel("T");
tSlider.setShowEndLabel(true);
zSlider.addPropertyChangeListener(this);
tSlider.addPropertyChangeListener(this);
zSlider.addChangeListener(this);
tSlider.addChangeListener(this);
mainPanel = new JPanel();
}
/** Builds and lays out the UI. */
private void buildGUI()
{
buildHistogramNoSelection();
setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));
JPanel centrePanel = new JPanel();
centrePanel.setLayout(new BoxLayout(centrePanel, BoxLayout.X_AXIS));
centrePanel.add(zSlider);
centrePanel.add(Box.createHorizontalStrut(5));
centrePanel.add(mainPanel);
centrePanel.add(export);
add(centrePanel);
add(tSlider);
}
/**
* Builds the default histogram when no channels are selected.
*
*/
private void buildHistogramNoSelection()
{
mainPanel.removeAll();
histogramChart = drawHistogram("Histogram", new ArrayList<String>(),
new ArrayList<double[]>(), new ArrayList<Color>(), 1001);
mainPanel.setLayout(new BorderLayout());
mainPanel.add(histogramChart.getChart(Collections.singletonList((AbstractAction)controller.getAction(MeasurementViewerControl.EXPORT_GRAPH))), BorderLayout.CENTER);
}
/**
* Draws the current data as a line plot in the graph.
*
* @param title The graph title.
* @param data The data to render.
* @param channelNames The channel names.
* @param channelColours The channel colours.
* @return See above.
*/
private LinePlot drawLineplot(String title, List<String> channelNames,
List<double[][]> data, List<Color> channelColours,
Map<Integer, List<String>> locations)
{
if (channelNames.size() == 0 || data.size() == 0 ||
channelColours.size() == 0)
return null;
if (channelNames.size() != channelColours.size() ||
channelNames.size() != data.size())
return null;
LinePlot plot = new LinePlot(title, channelNames, data,
channelColours, channelMinValue(), channelMaxValue());
plot.addLocations(locations);
plot.setYAxisName("Intensity");
plot.setXAxisName("Points");
return plot;
}
/**
* Draws the current data as a histogram in the graph.
*
* @param title The graph title.
* @param data The data to render.
* @param channelNames The channel names.
* @param channelColours The channel colours.
* @param bins The number of bins in the histogram.
* @return See above.
*/
private HistogramPlot drawHistogram(String title, List<String> channelNames,
List<double[]> data, List<Color> channelColours, int bins)
{
HistogramPlot plot;
if (CollectionUtils.isNotEmpty(data))
plot = new HistogramPlot(title, channelNames, data, channelColours,
bins, channelMinValue(), channelMaxValue());
else
plot = new HistogramPlot(title, Collections.EMPTY_LIST,
Collections.EMPTY_LIST, Collections.EMPTY_LIST, bins, 0, 1);
plot.setXAxisName("Intensity");
plot.setYAxisName("Frequency");
return plot;
}
/**
* The method builds the graphs from the data that was constructed in the
* display analysis method. This method should be called from either the
* display analysis method or the changelistener which uses the same ROI
* data generated in the displayAnalysis method.
*/
private void buildGraphsAndDisplay()
{
coord = new Coord3D(zSlider.getValue()-1, tSlider.getValue()-1);
Map<Integer, ROIShapeStatsSimple> data = pixelStats.get(coord);
if (data == null) return;
shape = shapeMap.get(coord);
double[][] dataXY;
Color c;
int channel;
List<double[]> channelData = new ArrayList<double[]>();
List<double[][]> channelXYData = new ArrayList<double[][]>();
channelName.clear();
channelColour.clear();
channelData.clear();
ChannelData cData;
List<ChannelData> metadata = model.getMetadata();
Iterator<ChannelData> j = metadata.iterator();
double[] values;
Map<Integer, List<String>> locations = new HashMap<Integer, List<String>>();
List<String> points = formatPoints(shape.getFigure().getPoints());
while (j.hasNext()) {
cData = j.next();
channel = cData.getIndex();
if (model.isChannelActive(channel))
{
cData = model.getMetadata(channel);
if (cData != null)
channelName.add(cData.getChannelLabeling());
c = model.getActiveChannelColor(channel);
if (UIUtilities.isSameColors(c, Color.white, false))
c = DEFAULT_COLOR;
channelColour.add(c);
values = data.get(channel).getValues();
if (values != null && values.length != 0) {
channelData.add(values);
if (lineProfileFigure(shape)) {
locations.put(channel, points);
dataXY = new double[2][values.length];
for (int i = 0 ; i < values.length ; i++)
{
dataXY[0][i] = i;
dataXY[1][i] = values[i];
}
channelXYData.add(dataXY);
}
}
}
}
mainPanel.removeAll();
if (channelData.size() == 0) {
buildHistogramNoSelection();
return;
}
lineProfileChart = null;
histogramChart = null;
if (lineProfileFigure(shape))
lineProfileChart = drawLineplot("Line Profile",
channelName, channelXYData, channelColour, locations);
histogramChart = drawHistogram("Histogram", channelName,
channelData, channelColour, 1001);
if (lineProfileChart == null && histogramChart !=null)
{
mainPanel.setLayout(new BorderLayout());
mainPanel.add(histogramChart.getChart(Collections.singletonList((AbstractAction)controller.getAction(MeasurementViewerControl.EXPORT_GRAPH))), BorderLayout.CENTER);
}
if (lineProfileChart != null && histogramChart !=null)
{
mainPanel.setLayout(new BoxLayout(mainPanel, BoxLayout.Y_AXIS));
mainPanel.add(lineProfileChart.getChart(Collections.singletonList((AbstractAction)controller.getAction(MeasurementViewerControl.EXPORT_GRAPH))));
mainPanel.add(histogramChart.getChart(Collections.singletonList((AbstractAction)controller.getAction(MeasurementViewerControl.EXPORT_GRAPH))));
}
mainPanel.validate();
mainPanel.repaint();
}
/**
* Formats the text associated to the specified points.
*
* @param points
* @return See above.
*/
private List<String> formatPoints(List<Point> points)
{
List<String> values = new ArrayList<String>();
Iterator<Point> i = points.iterator();
Point p;
StringBuilder b;
MeasurementUnits units = model.getMeasurementUnits();
double sx = units.getPixelSizeX().getValue();
double sy = units.getPixelSizeX().getValue();
while (i.hasNext()) {
p = i.next();
b = new StringBuilder();
b.append("("+p.x+", "+p.y+")"+UIUtilities.PIXELS_SYMBOL);
b.append("\n");
b.append("("+UIUtilities.twoDecimalPlaces(p.x*sx)+", "+
UIUtilities.twoDecimalPlaces(p.y*sy)+ ")"+EditorUtil.MICRONS_NO_BRACKET);
values.add(b.toString());
}
return values;
}
/** Indicates the selected plane.*/
private void formatPlane()
{
if (!zSlider.isVisible() && !tSlider.isVisible()) {
view.setPlaneStatus("");
return;
}
StringBuffer buffer = new StringBuffer();
if (zSlider.isVisible())
buffer.append("Z="+zSlider.getValue()+" ");
if (tSlider.isVisible())
buffer.append("T="+tSlider.getValue());
view.setPlaneStatus(buffer.toString());
}
/**
* Creates a new instance.
*
* @param view Reference to the View. Mustn't be <code>null</code>.
* @param controller Reference to the Control. Mustn't be <code>null</code>.
* @param model Reference to the Model. Mustn't be <code>null</code>.
*/
GraphPane(MeasurementViewerUI view, MeasurementViewerControl controller,
MeasurementViewerModel model)
{
if (view == null)
throw new IllegalArgumentException("No view.");
if (controller == null)
throw new IllegalArgumentException("No control.");
if (model == null)
throw new IllegalArgumentException("No model.");
this.model = model;
this.view = view;
this.controller = controller;
initComponents();
buildGUI();
}
/**
* Returns the name of the component.
*
* @return See above.
*/
String getComponentName() { return NAME; }
/**
* Returns the icon of the component.
*
* @return See above.
*/
Icon getComponentIcon()
{
IconManager icons = IconManager.getInstance();
return icons.getIcon(IconManager.GRAPHPANE);
}
/** Clears the data. */
void clearData()
{
mainPanel.removeAll();
if (zSlider != null) zSlider.setEnabled(false);
if (tSlider != null) tSlider.setEnabled(false);
}
/**
* Returns the analysis results from the model and converts to the
* necessary array. data types using the ROIStats wrapper then
* creates the graph and plot.
*/
void displayAnalysisResults()
{
this.ROIStats = model.getAnalysisResults();
if (ROIStats == null || ROIStats.size() == 0) {
buildHistogramNoSelection();
return;
}
shapeStatsList = new HashMap<Coord3D, Map<StatsType, Map>>();
pixelStats = new HashMap<Coord3D, Map<Integer, ROIShapeStatsSimple>>();
shapeMap = new HashMap<Coord3D, ROIShape>();
channelName = new ArrayList<String>();
channelColour = new ArrayList<Color>();
Entry entry;
Iterator i = ROIStats.entrySet().iterator();
int minZ = Integer.MAX_VALUE, maxZ = Integer.MIN_VALUE;
int minT = Integer.MAX_VALUE, maxT = Integer.MIN_VALUE;
Coord3D c3D;
Map<StatsType, Map> shapeStats;
Map<Integer, ROIShapeStatsSimple> data;
int t = model.getDefaultT();
int z = model.getDefaultZ();
boolean hasData = false;
int cT, cZ;
Set<Figure> statsMissingFigures = new HashSet<Figure>();
while (i.hasNext())
{
entry = (Entry) i.next();
shape = (ROIShape) entry.getKey();
c3D = shape.getCoord3D();
cT = c3D.getTimePoint();
cZ = c3D.getZSection();
if (cZ == z) {
minT = Math.min(minT, cT);
maxT = Math.max(maxT, cT);
}
if (cT == t) {
minZ = Math.min(minZ, cZ);
maxZ = Math.max(maxZ, cZ);
}
shapeMap.put(c3D, shape);
if (shape.getFigure() instanceof MeasureTextFigure)
return;
shapeStats = AnalysisStatsWrapper.convertStats(
(Map) entry.getValue());
if (cT == t && cZ == z) {
if (shapeStats != null)
// data for current plane is there, can be displayed
hasData = true;
else
// data is missing for current plane, analysis has to be
// kicked off for the specific figure
statsMissingFigures.add(shape.getFigure());
}
if (shapeStats != null) {
shapeStatsList.put(c3D, shapeStats);
data = shapeStats.get(StatsType.PIXELDATA);
pixelStats.put(c3D, data);
}
}
if (!hasData) {
if (!statsMissingFigures.isEmpty())
controller.analyseFigures(statsMissingFigures);
buildHistogramNoSelection();
return;
}
maxZ = maxZ+1;
minZ = minZ+1;
minT = minT+1;
maxT = maxT+1;
zSlider.removeChangeListener(this);
tSlider.removeChangeListener(this);
zSlider.setMaximum(maxZ);
zSlider.setMinimum(minZ);
tSlider.setMaximum(maxT);
tSlider.setMinimum(minT);
zSlider.setVisible(maxZ != minZ);
tSlider.setVisible(maxT != minT);
tSlider.setValue(model.getCurrentView().getTimePoint()+1);
zSlider.setValue(model.getCurrentView().getZSection()+1);
zSlider.addChangeListener(this);
tSlider.addChangeListener(this);
formatPlane();
buildGraphsAndDisplay();
}
/**
* Indicates any on-going analysis.
*
* @param analyse Passes <code>true</code> when analyzing,
* <code>false</code> otherwise.
*/
void onAnalysed(boolean analyse)
{
zSlider.setEnabled(!analyse);
tSlider.setEnabled(!analyse);
}
/**
* Reacts to changes made by slider.
* @see ChangeListener#stateChanged(ChangeEvent)
*/
public void stateChanged(ChangeEvent evt) {
Object src = evt.getSource();
if (src == zSlider || src == tSlider) {
formatPlane();
handleSliderReleased();
}
}
/**
* Listens to property fired by {@link #zSlider} or {@link #tSlider}.
* @see ChangeListener#stateChanged(ChangeEvent)
*/
public void propertyChange(PropertyChangeEvent evt)
{
String name = evt.getPropertyName();
if (OneKnobSlider.ONE_KNOB_RELEASED_PROPERTY.equals(name)) {
handleSliderReleased();
}
}
}