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