/* * Copyright (C) 2012 Brockmann Consult GmbH (info@brockmann-consult.de) * * 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 3 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, see http://www.gnu.org/licenses/ */ package org.esa.snap.rcp.statistics; import com.bc.ceres.binding.PropertyContainer; import com.bc.ceres.binding.PropertyDescriptor; import com.bc.ceres.binding.ValueRange; import com.bc.ceres.binding.validators.IntervalValidator; import com.bc.ceres.core.ProgressMonitor; import com.bc.ceres.core.SubProgressMonitor; import com.bc.ceres.swing.binding.BindingContext; import com.bc.ceres.swing.progress.ProgressMonitorSwingWorker; import org.esa.snap.core.datamodel.Mask; import org.esa.snap.core.datamodel.ProductNodeGroup; import org.esa.snap.core.datamodel.RasterDataNode; import org.esa.snap.core.datamodel.Stx; import org.esa.snap.core.datamodel.StxFactory; import org.esa.snap.core.datamodel.VectorDataNode; import org.esa.snap.core.util.StringUtils; import org.esa.snap.rcp.util.Dialogs; import org.esa.snap.statistics.output.Util; import org.esa.snap.ui.GridBagUtils; import org.esa.snap.ui.UIUtils; import org.esa.snap.ui.tool.ToolButtonFactory; import org.jfree.chart.ChartFactory; import org.jfree.chart.ChartPanel; import org.jfree.chart.JFreeChart; import org.jfree.chart.plot.PlotOrientation; import org.jfree.chart.plot.XYPlot; import org.jfree.chart.renderer.xy.StandardXYBarPainter; import org.jfree.chart.renderer.xy.XYBarRenderer; import org.jfree.data.xy.XIntervalSeries; import org.jfree.data.xy.XIntervalSeriesCollection; import org.jfree.ui.RectangleInsets; import org.openide.windows.TopComponent; import javax.media.jai.Histogram; import javax.swing.AbstractButton; import javax.swing.ImageIcon; import javax.swing.JCheckBox; import javax.swing.JLabel; import javax.swing.JLayeredPane; import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JPopupMenu; import javax.swing.JScrollPane; import javax.swing.JSeparator; import javax.swing.JSpinner; import javax.swing.JTable; import javax.swing.SpinnerNumberModel; import javax.swing.SwingWorker; import javax.swing.border.EmptyBorder; import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; import javax.swing.table.DefaultTableCellRenderer; import javax.swing.table.DefaultTableModel; import javax.swing.table.TableModel; import java.awt.BorderLayout; import java.awt.Color; import java.awt.Component; import java.awt.Dimension; import java.awt.GridBagConstraints; import java.awt.GridBagLayout; import java.awt.GridLayout; import java.awt.Rectangle; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.text.DecimalFormat; import java.util.List; /** * A general pane within the statistics window. * * @author Norman Fomferra * @author Marco Peters */ class StatisticsPanel extends PagePanel implements MultipleRoiComputePanel.ComputeMasks, StatisticsDataProvider { private static final String DEFAULT_STATISTICS_TEXT = "No statistics computed yet."; /*I18N*/ private static final String TITLE_PREFIX = "Statistics"; private MultipleRoiComputePanel computePanel; private JPanel backgroundPanel; private AbstractButton hideAndShowButton; private AbstractButton exportButton; private JPanel contentPanel; private final StatisticsPanel.PopupHandler popupHandler; private final StringBuilder resultText; private boolean init; private Histogram[] histograms; private ExportStatisticsAsCsvAction exportAsCsvAction; private PutStatisticsIntoVectorDataAction putStatisticsIntoVectorDataAction; private AccuracyModel accuracyModel; public StatisticsPanel(final TopComponent parentDialog, String helpID) { super(parentDialog, helpID, TITLE_PREFIX); setMinimumSize(new Dimension(1000, 390)); resultText = new StringBuilder(); popupHandler = new PopupHandler(); } @Override protected void initComponents() { init = true; computePanel = new MultipleRoiComputePanel(this, getRaster()); exportButton = getExportButton(); final JPanel exportAndHelpPanel = GridBagUtils.createPanel(); GridBagConstraints helpPanelConstraints = GridBagUtils.createConstraints("anchor=NORTHWEST,fill=HORIZONTAL,insets.top=2,weightx=1,ipadx=0"); GridBagUtils.addToPanel(exportAndHelpPanel, new JSeparator(), helpPanelConstraints, "fill=HORIZONTAL,gridwidth=2,insets.left=5,insets.right=5"); GridBagUtils.addToPanel(exportAndHelpPanel, exportButton, helpPanelConstraints, "gridy=1,anchor=WEST,fill=NONE"); GridBagUtils.addToPanel(exportAndHelpPanel, getHelpButton(), helpPanelConstraints, "gridx=1,gridy=1,anchor=EAST,fill=NONE"); final JPanel rightPanel = GridBagUtils.createPanel(); GridBagConstraints extendedOptionsPanelConstraints = GridBagUtils.createConstraints("anchor=NORTHWEST,fill=HORIZONTAL,insets.top=2,weightx=1,insets.right=-2"); GridBagUtils.addToPanel(rightPanel, computePanel, extendedOptionsPanelConstraints, "gridy=0,fill=BOTH,weighty=1"); GridBagUtils.addToPanel(rightPanel, createAccuracyPanel(), extendedOptionsPanelConstraints, "gridy=1,fill=BOTH,weighty=1"); GridBagUtils.addToPanel(rightPanel, exportAndHelpPanel, extendedOptionsPanelConstraints, "gridy=2,anchor=SOUTHWEST,fill=HORIZONTAL,weighty=0"); final ImageIcon collapseIcon = UIUtils.loadImageIcon("icons/PanelRight12.png"); final ImageIcon collapseRolloverIcon = ToolButtonFactory.createRolloverIcon(collapseIcon); final ImageIcon expandIcon = UIUtils.loadImageIcon("icons/PanelLeft12.png"); final ImageIcon expandRolloverIcon = ToolButtonFactory.createRolloverIcon(expandIcon); hideAndShowButton = ToolButtonFactory.createButton(collapseIcon, false); hideAndShowButton.setToolTipText("Collapse Options Panel"); hideAndShowButton.setName("switchToChartButton"); hideAndShowButton.addActionListener(new ActionListener() { public boolean rightPanelShown; @Override public void actionPerformed(ActionEvent e) { rightPanel.setVisible(rightPanelShown); if (rightPanelShown) { hideAndShowButton.setIcon(collapseIcon); hideAndShowButton.setRolloverIcon(collapseRolloverIcon); hideAndShowButton.setToolTipText("Collapse Options Panel"); } else { hideAndShowButton.setIcon(expandIcon); hideAndShowButton.setRolloverIcon(expandRolloverIcon); hideAndShowButton.setToolTipText("Expand Options Panel"); } rightPanelShown = !rightPanelShown; } }); contentPanel = new JPanel(new GridLayout(-1, 1)); contentPanel.setBackground(Color.WHITE); contentPanel.addMouseListener(popupHandler); final JScrollPane contentScrollPane = new JScrollPane(contentPanel); contentScrollPane.setBorder(null); contentScrollPane.setBackground(Color.WHITE); backgroundPanel = new JPanel(new GridBagLayout()); GridBagConstraints gbc = new GridBagConstraints(); GridBagUtils.addToPanel(backgroundPanel, contentScrollPane, gbc, "fill=BOTH, weightx=1.0, weighty=1.0, anchor=NORTH"); GridBagUtils.addToPanel(backgroundPanel, rightPanel, gbc, "gridx=1, fill=VERTICAL, weightx=0.0"); JLayeredPane layeredPane = new JLayeredPane(); layeredPane.add(backgroundPanel); layeredPane.add(hideAndShowButton); add(layeredPane); } private JPanel createAccuracyPanel() { final JPanel accuracyPanel = new JPanel(new GridBagLayout()); final GridBagConstraints gbc = new GridBagConstraints(); final JLabel label = new JLabel("Histogram accuracy:"); accuracyModel = new AccuracyModel(); final BindingContext bindingContext = new BindingContext(PropertyContainer.createObjectBacked(accuracyModel)); final SpinnerNumberModel accuracyNumberModel = new SpinnerNumberModel(accuracyModel.accuracy, 0, Util.MAX_ACCURACY, 1); final JSpinner accuracySpinner = new JSpinner(accuracyNumberModel); ((JSpinner.DefaultEditor) accuracySpinner.getEditor()).getTextField().setEditable(false); bindingContext.bind("accuracy", accuracySpinner); final JCheckBox checkBox = new JCheckBox("Auto accuracy"); bindingContext.bind("useAutoAccuracy", checkBox); final IntervalValidator rangeValidator = new IntervalValidator(new ValueRange(0, Util.MAX_ACCURACY)); final PropertyDescriptor accuracyDescriptor = bindingContext.getPropertySet().getDescriptor("accuracy"); accuracyDescriptor.setValidator(rangeValidator); checkBox.setSelected(accuracyModel.useAutoAccuracy); bindingContext.getPropertySet().getProperty("useAutoAccuracy").addPropertyChangeListener(new PropertyChangeListener() { @Override public void propertyChange(PropertyChangeEvent evt) { label.setEnabled(!checkBox.isSelected()); accuracySpinner.setEnabled(!checkBox.isSelected()); if (checkBox.isSelected()) { bindingContext.getBinding("accuracy").setPropertyValue(3); } computePanel.updateEnablement(); } }); label.setEnabled(false); accuracySpinner.setEnabled(false); accuracySpinner.setToolTipText("Specify the number of histogram bins (#bins: 10^accuracy)."); accuracySpinner.addChangeListener(new ChangeListener() { @Override public void stateChanged(ChangeEvent e) { computePanel.updateEnablement(); } }); GridBagUtils.addToPanel(accuracyPanel, new TitledSeparator("Histogram accuracy"), gbc, "fill=HORIZONTAL, weightx=1.0,anchor=NORTH,gridwidth=2"); GridBagUtils.addToPanel(accuracyPanel, checkBox, gbc, "gridy=1,insets.left=5,insets.top=2"); GridBagUtils.addToPanel(accuracyPanel, label, gbc, "gridy=2, insets.left=26,weightx=0.0,fill=NONE,anchor=WEST,gridwidth=1"); GridBagUtils.addToPanel(accuracyPanel, accuracySpinner, gbc, "gridx=1,weightx=1.0,fill=HORIZONTAL,insets.right=5,insets.left=5"); return accuracyPanel; } @Override protected void updateComponents() { if (!init) { initComponents(); } final RasterDataNode raster = getRaster(); computePanel.setRaster(raster); contentPanel.removeAll(); resultText.setLength(0); if (raster != null && raster.isStxSet() && raster.getStx().getResolutionLevel() == 0) { resultText.append(createText(raster.getStx(), null)); contentPanel.add(createStatPanel(raster.getStx(), null)); histograms = new Histogram[]{raster.getStx().getHistogram()}; exportAsCsvAction = new ExportStatisticsAsCsvAction(this); putStatisticsIntoVectorDataAction = new PutStatisticsIntoVectorDataAction(this); exportButton.setEnabled(true); } else { contentPanel.add(new JLabel(DEFAULT_STATISTICS_TEXT)); exportButton.setEnabled(false); } contentPanel.revalidate(); contentPanel.repaint(); } @Override public Histogram[] getHistograms() { return histograms; } private static class ComputeResult { final Stx stx; final Mask mask; ComputeResult(Stx stx, Mask mask) { this.stx = stx; this.mask = mask; } } @Override public void compute(final Mask[] selectedMasks) { this.histograms = new Histogram[selectedMasks.length]; final String title = "Computing Statistics"; SwingWorker<Object, ComputeResult> swingWorker = new ProgressMonitorSwingWorker<Object, ComputeResult>(this, title) { @Override protected Object doInBackground(ProgressMonitor pm) { pm.beginTask(title, selectedMasks.length); try { final int binCount = Util.computeBinCount(accuracyModel.accuracy); for (int i = 0; i < selectedMasks.length; i++) { final Mask mask = selectedMasks[i]; final Stx stx; ProgressMonitor subPm = SubProgressMonitor.create(pm, 1); if (mask == null) { stx = new StxFactory() .withHistogramBinCount(binCount) .create(getRaster(), subPm); getRaster().setStx(stx); } else { stx = new StxFactory() .withHistogramBinCount(binCount) .withRoiMask(mask) .create(getRaster(), subPm); } histograms[i] = stx.getHistogram(); publish(new ComputeResult(stx, mask)); } } finally { pm.done(); } return null; } @Override protected void process(List<ComputeResult> chunks) { for (ComputeResult result : chunks) { final Stx stx = result.stx; final Mask mask = result.mask; if (resultText.length() > 0) { resultText.append("\n"); } resultText.append(createText(stx, mask)); JPanel statPanel = createStatPanel(stx, mask); contentPanel.add(statPanel); contentPanel.revalidate(); contentPanel.repaint(); } } @Override protected void done() { try { get(); if (exportAsCsvAction == null) { exportAsCsvAction = new ExportStatisticsAsCsvAction(StatisticsPanel.this); } exportAsCsvAction.setSelectedMasks(selectedMasks); if (putStatisticsIntoVectorDataAction == null) { putStatisticsIntoVectorDataAction = new PutStatisticsIntoVectorDataAction(StatisticsPanel.this); } putStatisticsIntoVectorDataAction.setSelectedMasks(selectedMasks); exportButton.setEnabled(true); } catch (Exception e) { e.printStackTrace(); Dialogs.showMessage("<html>Statistics", "Failed to compute statistics.<br/>An error occurred:" + e.getMessage() + "</html>", JOptionPane.ERROR_MESSAGE, null); } } }; resultText.setLength(0); contentPanel.removeAll(); swingWorker.execute(); } private JPanel createStatPanel(Stx stx, final Mask mask) { final Histogram histogram = stx.getHistogram(); XIntervalSeries histogramSeries = new XIntervalSeries("Histogram"); int[] bins = histogram.getBins(0); for (int j = 0; j < bins.length; j++) { histogramSeries.add(histogram.getBinLowValue(0, j), histogram.getBinLowValue(0, j), j < bins.length - 1 ? histogram.getBinLowValue(0, j + 1) : histogram.getHighValue(0), bins[j]); } ChartPanel histogramPanel = createChartPanel(histogramSeries, "Value", "#Pixels", new Color(0, 0, 127)); XIntervalSeries percentileSeries = new XIntervalSeries("Percentile"); percentileSeries.add(0, 0, 1, histogram.getLowValue(0)); for (int j = 1; j < 99; j++) { percentileSeries.add(j, j, j + 1, histogram.getPTileThreshold(j / 100.0)[0]); } percentileSeries.add(99, 99, 100, histogram.getHighValue(0)); ChartPanel percentilePanel = createChartPanel(percentileSeries, "Percentile (%)", "Value Threshold", new Color(127, 0, 0)); Object[][] tableData = new Object[][]{ new Object[]{"#Pixels total:", histogram.getTotals()[0]}, new Object[]{"Minimum:", stx.getMinimum()}, new Object[]{"Maximum:", stx.getMaximum()}, new Object[]{"Mean:", stx.getMean()}, new Object[]{"Sigma:", stx.getStandardDeviation()}, new Object[]{"Median:", stx.getMedian()}, new Object[]{"Coef Variation:", stx.getCoefficientOfVariation()}, new Object[]{"ENL:", stx.getEquivalentNumberOfLooks()}, new Object[]{"P75 threshold:", histogram.getPTileThreshold(0.75)[0]}, new Object[]{"P80 threshold:", histogram.getPTileThreshold(0.80)[0]}, new Object[]{"P85 threshold:", histogram.getPTileThreshold(0.85)[0]}, new Object[]{"P90 threshold:", histogram.getPTileThreshold(0.90)[0]}, new Object[]{"Max error:", getBinSize(histogram)}, }; JPanel plotContainerPanel = new JPanel(new GridLayout(1, 2)); plotContainerPanel.add(histogramPanel); plotContainerPanel.add(percentilePanel); TableModel tableModel = new DefaultTableModel(tableData, new String[]{"Name", "Value"}) { @Override public Class<?> getColumnClass(int columnIndex) { return columnIndex == 0 ? String.class : Number.class; } @Override public boolean isCellEditable(int row, int column) { return false; } }; final JTable table = new JTable(tableModel); table.setDefaultRenderer(Number.class, new DefaultTableCellRenderer() { @Override public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) { final Component label = super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column); if (value instanceof Float || value instanceof Double) { setHorizontalTextPosition(RIGHT); setText(getFormattedValue((Number) value)); } return label; } private String getFormattedValue(Number value) { if (value.doubleValue() < 0.001 && value.doubleValue() > -0.001 && value.doubleValue() != 0.0) { return new DecimalFormat("0.####E0").format(value.doubleValue()); } return String.format("%.4f", value.doubleValue()); } }); table.addMouseListener(popupHandler); JPanel textContainerPanel = new JPanel(new BorderLayout(2, 2)); textContainerPanel.setBackground(Color.WHITE); textContainerPanel.add(table, BorderLayout.CENTER); JPanel statPanel = new JPanel(new BorderLayout(4, 4)); statPanel.setBorder(new EmptyBorder(10, 2, 10, 2)); statPanel.setBackground(Color.WHITE); statPanel.add(new JLabel(getSubPanelTitle(mask)), BorderLayout.NORTH); statPanel.add(textContainerPanel, BorderLayout.WEST); statPanel.add(plotContainerPanel, BorderLayout.CENTER); return statPanel; } static double getBinSize(Histogram histogram) { return (histogram.getHighValue(0) - histogram.getLowValue(0)) / histogram.getNumBins(0); } private String getSubPanelTitle(Mask mask) { final String title; if (mask != null) { title = String.format("<html><b>%s</b> with ROI-mask <b>%s</b></html>", getRaster().getName(), mask.getName()); } else { title = String.format("<html><b>%s</b></html>", getRaster().getName()); } return title; } @Override protected String getDataAsText() { return resultText.toString(); } private String createText(final Stx stx, final Mask mask) { if (stx.getSampleCount() == 0) { if (mask != null) { return "The ROI-Mask '" + mask.getName() + "' is empty."; } else { return "The scene contains no valid pixels."; } } RasterDataNode raster = getRaster(); boolean maskUsed = mask != null; final String unit = (StringUtils.isNotNullAndNotEmpty(raster.getUnit()) ? raster.getUnit() : "1"); final long numPixelTotal = (long) raster.getRasterWidth() * (long) raster.getRasterHeight(); final StringBuilder sb = new StringBuilder(1024); sb.append("Only ROI-mask pixels considered:\t"); sb.append(maskUsed ? "Yes" : "No"); sb.append("\n"); if (maskUsed) { sb.append("ROI-mask name:\t"); sb.append(mask.getName()); sb.append("\n"); } sb.append("Number of pixels total:\t"); sb.append(numPixelTotal); sb.append("\n"); sb.append("Number of considered pixels:\t"); sb.append(stx.getSampleCount()); sb.append("\n"); sb.append("Ratio of considered pixels:\t"); sb.append(100.0 * stx.getSampleCount() / numPixelTotal); sb.append("\t"); sb.append("%"); sb.append("\n"); sb.append("Minimum:\t"); sb.append(stx.getMinimum()); sb.append("\t"); sb.append(unit); sb.append("\n"); sb.append("Maximum:\t"); sb.append(stx.getMaximum()); sb.append("\t"); sb.append(unit); sb.append("\n"); sb.append("Mean:\t"); sb.append(stx.getMean()); sb.append("\t"); sb.append(unit); sb.append("\n"); sb.append("Standard deviation:\t"); sb.append(stx.getStandardDeviation()); sb.append("\t"); sb.append(unit); sb.append("\n"); sb.append("Coefficient of variation:\t"); sb.append(getCoefficientOfVariation(stx)); sb.append("\t"); sb.append(""); sb.append("\n"); sb.append("Median:\t"); sb.append(stx.getMedian()); sb.append("\t "); sb.append(unit); sb.append("\n"); for (int percentile = 5; percentile <= 95; percentile += 5) { sb.append("P").append(percentile).append(" threshold:\t"); sb.append(stx.getHistogram().getPTileThreshold(percentile / 100.0)[0]); sb.append("\t"); sb.append(unit); sb.append("\n"); } sb.append("Threshold max error:\t"); sb.append(getBinSize(stx.getHistogram())); sb.append("\t"); sb.append(unit); sb.append("\n"); return sb.toString(); } private double getCoefficientOfVariation(Stx stx) { return stx.getStandardDeviation() / stx.getMean(); } @Override public void doLayout() { super.doLayout(); backgroundPanel.setBounds(0, 0, getWidth() - 8, getHeight() - 8); hideAndShowButton.setBounds(getWidth() - hideAndShowButton.getWidth() - 12, 6, 24, 24); } private static ChartPanel createChartPanel(XIntervalSeries percentileSeries, String xAxisLabel, String yAxisLabel, Color color) { XIntervalSeriesCollection percentileDataset = new XIntervalSeriesCollection(); percentileDataset.addSeries(percentileSeries); return getHistogramPlotPanel(percentileDataset, xAxisLabel, yAxisLabel, color); } private static ChartPanel getHistogramPlotPanel(XIntervalSeriesCollection dataset, String xAxisLabel, String yAxisLabel, Color color) { JFreeChart chart = ChartFactory.createHistogram( null, xAxisLabel, yAxisLabel, dataset, PlotOrientation.VERTICAL, false, // Legend? true, // tooltips false // url ); final XYPlot xyPlot = chart.getXYPlot(); //xyPlot.setForegroundAlpha(0.85f); xyPlot.setNoDataMessage("No data"); xyPlot.setAxisOffset(new RectangleInsets(5, 5, 5, 5)); final XYBarRenderer renderer = (XYBarRenderer) xyPlot.getRenderer(); renderer.setDrawBarOutline(false); renderer.setShadowVisible(false); renderer.setSeriesPaint(0, color); StandardXYBarPainter painter = new StandardXYBarPainter(); renderer.setBarPainter(painter); ChartPanel chartPanel = new ChartPanel(chart); chartPanel.setPreferredSize(new Dimension(300, 200)); // chartPanel.getPopupMenu().add(createCopyDataToClipboardMenuItem()); return chartPanel; } private AbstractButton getExportButton() { final AbstractButton export = ToolButtonFactory.createButton(UIUtils.loadImageIcon("icons/Export24.gif"), false); export.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { JPopupMenu viewPopup = new JPopupMenu("Export"); viewPopup.add(exportAsCsvAction); viewPopup.add(putStatisticsIntoVectorDataAction); final Rectangle buttonBounds = export.getBounds(); viewPopup.show(export, 1, buttonBounds.height + 1); } }); export.setEnabled(false); return export; } @Override public RasterDataNode getRasterDataNode() { return getRaster(); } @Override public ProductNodeGroup<VectorDataNode> getVectorDataNodeGroup() { return getRasterDataNode().getProduct().getVectorDataGroup(); } private class PopupHandler extends MouseAdapter { @Override public void mouseReleased(MouseEvent e) { if (e.getButton() == 2 || e.isPopupTrigger()) { final JPopupMenu menu = new JPopupMenu(); menu.add(createCopyDataToClipboardMenuItem()); menu.show(e.getComponent(), e.getX(), e.getY()); } } } // The fields of this class are used by the binding framework @SuppressWarnings("UnusedDeclaration") static class AccuracyModel { private int accuracy = 3; private boolean useAutoAccuracy = true; } }