/* * 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.ValidationException; import com.bc.ceres.binding.ValueRange; import com.bc.ceres.core.ProgressMonitor; import com.bc.ceres.swing.binding.Binding; import com.bc.ceres.swing.binding.BindingContext; import com.bc.ceres.swing.binding.Enablement; import com.bc.ceres.swing.progress.ProgressMonitorSwingWorker; import org.esa.snap.core.datamodel.Mask; import org.esa.snap.core.datamodel.Product; import org.esa.snap.core.datamodel.ProductManager; import org.esa.snap.core.datamodel.ProductNode; import org.esa.snap.core.datamodel.ProductNodeEvent; 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.dataop.barithm.BandArithmetic; import org.esa.snap.rcp.SnapApp; import org.esa.snap.rcp.util.Dialogs; import org.esa.snap.ui.GridBagUtils; import org.jfree.chart.ChartFactory; import org.jfree.chart.ChartPanel; import org.jfree.chart.JFreeChart; import org.jfree.chart.axis.ValueAxis; 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.JCheckBox; import javax.swing.JLabel; import javax.swing.JPanel; import javax.swing.JTextField; import javax.swing.SwingUtilities; import java.awt.BasicStroke; import java.awt.Color; import java.awt.Dimension; import java.awt.GridBagConstraints; import java.awt.Shape; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.geom.Rectangle2D; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.util.concurrent.ExecutionException; /** * A pane within the statistics window which displays a histogram. */ class HistogramPanel extends ChartPagePanel { private static final String NO_DATA_MESSAGE = "No histogram computed yet.\n" + "If a band is selected, a histogram can be created by hitting the 'Refresh View' button.\n" + HELP_TIP_MESSAGE + "\n" + ZOOM_TIP_MESSAGE; private static final String CHART_TITLE = "Histogram"; public static final String PROPERTY_NAME_NUM_BINS = "numBins"; public static final String PROPERTY_NAME_LOGARITHMIC_HISTOGRAM = "histogramLogScaled"; public static final String PROPERTY_NAME_LOG_SCALED = "xAxisLogScaled"; private static final double HISTO_MIN_DEFAULT = 0.0; private static final double HISTO_MAX_DEFAULT = 100.0; private static final int NUM_BINS_DEFAULT = 512; private AxisRangeControl xAxisRangeControl; private XIntervalSeriesCollection dataset; private JFreeChart chart; private HistogramPlotConfig histogramPlotConfig; private BindingContext bindingContext; private boolean isInitialized; private boolean histogramComputing; private Enablement log10AxisEnablement; private Enablement log10HistEnablement; private HistogramPanelModel model; private HistogramPanel.ConfigChangeListener configChangeListener; HistogramPanel(final TopComponent parentComponent, String helpID) { super(parentComponent, helpID, CHART_TITLE, true); } @Override protected void initComponents() { SnapApp.getDefault().getSelectionSupport(ProductNode.class).addHandler((oldValue, newValue) -> { if (newValue != null) { handleMasklessProduct(newValue.getProduct()); } }); SnapApp.getDefault().getProductManager().addListener(new ProductManager.Listener() { @Override public void productAdded(ProductManager.Event event) { //do nothing } @Override public void productRemoved(ProductManager.Event event) { model.removeStxFromProduct(event.getProduct()); } }); model = new HistogramPanelModel(); xAxisRangeControl = new AxisRangeControl("X-Axis"); histogramPlotConfig = new HistogramPlotConfig(); bindingContext = new BindingContext(PropertyContainer.createObjectBacked(histogramPlotConfig)); configChangeListener = new ConfigChangeListener(); bindingContext.addPropertyChangeListener(configChangeListener); createUI(); updateComponents(); } @Override protected void updateComponents() { if (!isInitialized || !isVisible()) { return; } super.updateComponents(); chart.setTitle(getRaster() != null ? CHART_TITLE + " for " + getRaster().getName() : CHART_TITLE); updateXAxis(); if (xAxisRangeControl.isAutoMinMax()) { xAxisRangeControl.getBindingContext().getPropertySet().getDescriptor("min").setDefaultValue( HISTO_MIN_DEFAULT); xAxisRangeControl.getBindingContext().getPropertySet().getDescriptor("max").setDefaultValue( HISTO_MAX_DEFAULT); } dataset = null; handleStxChange(); updateRefreshButton(); } private void handleMasklessProduct(Product product) { if (product != null && product.getMaskGroup().getNodeCount() == 0) { try { bindingContext.getPropertySet().getProperty("useRoiMask").setValue(Boolean.FALSE); } catch (ValidationException e) { throw new IllegalStateException("Cannot come here"); } } } private void updateRefreshButton() { refreshButton.setEnabled(getRaster() != null && !model.hasStx(createHistogramConfig())); } @Override protected boolean mustHandleSelectionChange() { return isRasterChanged(); } @Override protected void handleNodeSelectionChanged() { super.handleNodeSelectionChanged(); handleMasklessProduct(getProduct()); SwingUtilities.invokeLater(new Runnable() { @Override public void run() { updateChartData(false); } }); } @Override public void nodeDataChanged(ProductNodeEvent event) { super.nodeDataChanged(event); if (!histogramPlotConfig.useRoiMask) { return; } final Mask roiMask = histogramPlotConfig.roiMask; if (roiMask == null) { return; } final ProductNode sourceNode = event.getSourceNode(); if (!(sourceNode instanceof Mask)) { return; } final String maskName = sourceNode.getName(); if (roiMask.getName().equals(maskName)) { model.removeStx(createHistogramConfig()); updateComponents(); } } private void createUI() { dataset = new XIntervalSeriesCollection(); chart = ChartFactory.createHistogram( CHART_TITLE, "Values", "Frequency in #pixels", dataset, PlotOrientation.VERTICAL, false, // Legend? true, // tooltips false // url ); final XYPlot xyPlot = chart.getXYPlot(); xyPlot.setDomainZeroBaselineStroke(new BasicStroke(0.2f)); final XYBarRenderer renderer = (XYBarRenderer) xyPlot.getRenderer(); renderer.setDrawBarOutline(false); renderer.setShadowVisible(false); renderer.setShadowYOffset(-4.0); renderer.setBaseToolTipGenerator(new XYPlotToolTipGenerator()); renderer.setBarPainter(new StandardXYBarPainter()); renderer.setSeriesPaint(0, new Color(0, 0, 200)); createUI(createChartPanel(chart), createOptionsPanel(), bindingContext); isInitialized = true; final Binding minBinding = xAxisRangeControl.getBindingContext().getBinding("min"); final double min = (Double) minBinding.getPropertyValue(); final Binding maxBinding = xAxisRangeControl.getBindingContext().getBinding("max"); final double max = (Double) maxBinding.getPropertyValue(); if (!histogramComputing && min > max) { minBinding.setPropertyValue(max); maxBinding.setPropertyValue(min); } updateXAxis(); } private JPanel createOptionsPanel() { final JLabel numBinsLabel = new JLabel("#Bins:"); JTextField numBinsField = new JTextField(Integer.toString(NUM_BINS_DEFAULT)); numBinsField.setPreferredSize(new Dimension(50, numBinsField.getPreferredSize().height)); final JCheckBox histoLogCheck = new JCheckBox("Log10 scaled bins"); histoLogCheck.addActionListener(configChangeListener); bindingContext.getPropertySet().getDescriptor(PROPERTY_NAME_NUM_BINS).setDescription( "Set the number of bins in the histogram"); bindingContext.getPropertySet().getDescriptor(PROPERTY_NAME_NUM_BINS).setValueRange( new ValueRange(2.0, 2048.0)); bindingContext.getPropertySet().getDescriptor(PROPERTY_NAME_NUM_BINS).setDefaultValue(NUM_BINS_DEFAULT); bindingContext.bind(PROPERTY_NAME_NUM_BINS, numBinsField); bindingContext.getPropertySet().getDescriptor(PROPERTY_NAME_LOGARITHMIC_HISTOGRAM).setDescription( "Use log-10 scaled values for computation of histogram"); bindingContext.getPropertySet().getDescriptor(PROPERTY_NAME_LOGARITHMIC_HISTOGRAM).setDefaultValue(false); bindingContext.bind(PROPERTY_NAME_LOGARITHMIC_HISTOGRAM, histoLogCheck); log10HistEnablement = bindingContext.bindEnabledState(PROPERTY_NAME_LOGARITHMIC_HISTOGRAM, true, new Enablement.Condition() { @Override public boolean evaluate(BindingContext bindingContext) { return getRaster() != null && getRaster().getStx().getMaximum() > 0; } }); PropertyChangeListener logChangeListener = new AxisControlChangeListener(); xAxisRangeControl.getBindingContext().addPropertyChangeListener(logChangeListener); xAxisRangeControl.getBindingContext().getPropertySet().addProperty( bindingContext.getPropertySet().getProperty(PROPERTY_NAME_LOGARITHMIC_HISTOGRAM)); xAxisRangeControl.getBindingContext().getPropertySet().addProperty( bindingContext.getPropertySet().getProperty(PROPERTY_NAME_LOG_SCALED)); xAxisRangeControl.getBindingContext().getPropertySet().getDescriptor(PROPERTY_NAME_LOG_SCALED).setDescription( "Toggle whether to use a logarithmic x-axis"); log10AxisEnablement = xAxisRangeControl.getBindingContext().bindEnabledState(PROPERTY_NAME_LOG_SCALED, true, new Enablement.Condition() { @Override public boolean evaluate(BindingContext bindingContext) { HistogramPanelModel.HistogramConfig currentConfig = createHistogramConfig(); boolean hasStx = model.hasStx(currentConfig); // log10 xAxis is enabled when current histogram exists and is NOT log10 scaled return dataset != null && hasStx && !model.getStx(currentConfig).isLogHistogram(); } }); JPanel dataSourceOptionsPanel = GridBagUtils.createPanel(); GridBagConstraints dataSourceOptionsConstraints = GridBagUtils.createConstraints( "anchor=NORTHWEST,fill=HORIZONTAL,insets.top=2"); GridBagUtils.addToPanel(dataSourceOptionsPanel, new JLabel(" "), dataSourceOptionsConstraints, "gridwidth=2,gridy=0,gridx=0,weightx=0"); GridBagUtils.addToPanel(dataSourceOptionsPanel, numBinsLabel, dataSourceOptionsConstraints, "insets.top=2,insets.left=4,gridwidth=1,gridy=1,gridx=0,weightx=1"); GridBagUtils.addToPanel(dataSourceOptionsPanel, numBinsField, dataSourceOptionsConstraints, "insets.top=0,insets.left=0,insets.right=2,gridwidth=1,gridy=1,gridx=1"); GridBagUtils.addToPanel(dataSourceOptionsPanel, histoLogCheck, dataSourceOptionsConstraints, "insets.right=0,gridwidth=2,gridy=2,gridx=0"); xAxisRangeControl.getBindingContext().bind(PROPERTY_NAME_LOG_SCALED, new JCheckBox("Log10 scaled")); xAxisRangeControl.getBindingContext().addPropertyChangeListener(PROPERTY_NAME_LOG_SCALED, new PropertyChangeListener() { @Override public void propertyChange(PropertyChangeEvent evt) { ValueAxis oldAxis = chart.getXYPlot().getDomainAxis(); ValueAxis newAxis = StatisticChartStyling.updateScalingOfAxis((Boolean) evt.getNewValue(), oldAxis, true); chart.getXYPlot().setDomainAxis(newAxis); } }); JPanel displayOptionsPanel = GridBagUtils.createPanel(); GridBagConstraints displayOptionsConstraints = GridBagUtils.createConstraints( "anchor=SOUTH,fill=HORIZONTAL,weightx=1"); GridBagUtils.addToPanel(displayOptionsPanel, xAxisRangeControl.getPanel(), displayOptionsConstraints, "gridy=2"); JPanel optionsPanel = GridBagUtils.createPanel(); GridBagConstraints gbc = GridBagUtils.createConstraints( "anchor=NORTHWEST,fill=HORIZONTAL,insets.top=2,weightx=1"); GridBagUtils.addToPanel(optionsPanel, dataSourceOptionsPanel, gbc, "gridy=0"); GridBagUtils.addToPanel(optionsPanel, new JPanel(), gbc, "gridy=1,fill=VERTICAL,weighty=1"); GridBagUtils.addToPanel(optionsPanel, displayOptionsPanel, gbc, "gridy=2,fill=HORIZONTAL,weighty=0"); GridBagUtils.addToPanel(optionsPanel, new JPanel(), gbc, "gridy=3,fill=VERTICAL,weighty=1"); GridBagUtils.addToPanel(optionsPanel, xAxisRangeControl.getBindingContext().getBinding( PROPERTY_NAME_LOG_SCALED).getComponents()[0], gbc, "gridy=4"); return optionsPanel; } private HistogramPanelModel.HistogramConfig createHistogramConfig() { if (getRaster() == null || isRasterChanged()) { return null; } return new HistogramPanelModel.HistogramConfig(getRaster(), histogramPlotConfig.useRoiMask ? histogramPlotConfig.roiMask.getName() : null, histogramPlotConfig.numBins, histogramPlotConfig.histogramLogScaled); } private ChartPanel createChartPanel(JFreeChart chart) { XYPlot plot = chart.getXYPlot(); plot.setForegroundAlpha(0.85f); plot.setNoDataMessage(NO_DATA_MESSAGE); plot.setAxisOffset(new RectangleInsets(5, 5, 5, 5)); ChartPanel chartPanel = new ChartPanel(chart); MaskSelectionToolSupport maskSelectionToolSupport = new MaskSelectionToolSupport(this, chartPanel, "histogram_plot_area", "Mask generated from selected histogram plot area", Color.RED, PlotAreaSelectionTool.AreaType.X_RANGE) { @Override protected String createMaskExpression(PlotAreaSelectionTool.AreaType areaType, Shape shape) { Rectangle2D bounds = shape.getBounds2D(); return createMaskExpression(bounds.getMinX(), bounds.getMaxX()); } protected String createMaskExpression(double x1, double x2) { String bandName = BandArithmetic.createExternalName(getRaster().getName()); HistogramPanelModel.HistogramConfig currentConfig = createHistogramConfig(); return String.format("%s >= %s && %s <= %s", bandName, model.hasStx(currentConfig) ? model.getStx(currentConfig).getHistogramScaling().scaleInverse(x1) : x1, bandName, model.hasStx(currentConfig) ? model.getStx(currentConfig).getHistogramScaling().scaleInverse(x2) : x2); } }; chartPanel.getPopupMenu().addSeparator(); chartPanel.getPopupMenu().add(maskSelectionToolSupport.createMaskSelectionModeMenuItem()); chartPanel.getPopupMenu().add(maskSelectionToolSupport.createDeleteMaskMenuItem()); chartPanel.getPopupMenu().addSeparator(); chartPanel.getPopupMenu().add(createCopyDataToClipboardMenuItem()); return chartPanel; } @SuppressWarnings("UnusedDeclaration") // will be used as property container for binding context private static class HistogramPlotConfig { private boolean xAxisLogScaled; private boolean histogramLogScaled; private int numBins = NUM_BINS_DEFAULT; private boolean useRoiMask; private Mask roiMask; } @Override public void updateChartData() { updateChartData(true); } private void updateChartData(boolean recompute) { final boolean autoMinMaxEnabled = getAutoMinMaxEnabled(); final Double min; final Double max; if (autoMinMaxEnabled) { min = null; max = null; } else { min = (Double) xAxisRangeControl.getBindingContext().getBinding("min").getPropertyValue(); max = (Double) xAxisRangeControl.getBindingContext().getBinding("max").getPropertyValue(); } new StxWorker(min, max, autoMinMaxEnabled, recompute).execute(); } private void setStx(Stx stx) { if (stx != null) { HistogramPanelModel.HistogramConfig config = createHistogramConfig(); if (config == null) { return; } if (!model.hasStx(config)) { model.setStx(config, stx); } dataset = new XIntervalSeriesCollection(); final int[] binCounts = stx.getHistogramBins(); final RasterDataNode raster = getRaster(); final XIntervalSeries series = new XIntervalSeries(raster.getName()); final Histogram histogram = stx.getHistogram(); for (int i = 0; i < binCounts.length; i++) { final double xMin = histogram.getBinLowValue(0, i); final double xMax = i < binCounts.length - 1 ? histogram.getBinLowValue(0, i + 1) : histogram.getHighValue(0); series.add(xMin, xMin, xMax, binCounts[i]); } dataset.addSeries(series); } handleStxChange(); } private void handleStxChange() { if (model.hasStx(createHistogramConfig())) { refreshButton.setEnabled(false); } log10HistEnablement.apply(); updateLogXAxisCheckBox(); chart.getXYPlot().setDataset(dataset); updateXAxis(); chart.fireChartChanged(); } private String getAxisLabel() { boolean logScaled = (Boolean) bindingContext.getBinding(PROPERTY_NAME_LOGARITHMIC_HISTOGRAM).getPropertyValue(); return StatisticChartStyling.getAxisLabel(getRaster(), "X", logScaled); } private boolean getAutoMinMaxEnabled() { return xAxisRangeControl.isAutoMinMax(); } @Override public String getDataAsText() { HistogramPanelModel.HistogramConfig config = createHistogramConfig(); if (!model.hasStx(config)) { return null; } Stx stx = model.getStx(config); final int[] binVals = stx.getHistogramBins(); final int numBins = binVals.length; final double min = stx.getMinimum(); final double max = stx.getMaximum(); final StringBuilder sb = new StringBuilder(16000); sb.append("Product name:\t").append(getRaster().getProduct().getName()).append("\n"); sb.append("Dataset name:\t").append(getRaster().getName()).append("\n"); sb.append('\n'); sb.append("Histogram minimum:\t").append(min).append("\t").append(getRaster().getUnit()).append("\n"); sb.append("Histogram maximum:\t").append(max).append("\t").append(getRaster().getUnit()).append("\n"); sb.append("Histogram bin size:\t").append( getRaster().isLog10Scaled() ? ("NA\t") : ((max - min) / numBins + "\t") + getRaster().getUnit() + "\n"); sb.append("Histogram #bins:\t").append(numBins).append("\n"); sb.append('\n'); sb.append("Bin center value"); sb.append('\t'); sb.append("Bin counts"); sb.append('\n'); for (int i = 0; i < numBins; i++) { sb.append(min + ((i + 0.5) * (max - min)) / numBins); sb.append('\t'); sb.append(binVals[i]); sb.append('\n'); } return sb.toString(); } private void updateLogXAxisCheckBox() { HistogramPanelModel.HistogramConfig config = createHistogramConfig(); final boolean enabled = dataset != null && model.hasStx(config) && model.getStx(config).getMinimum() > 0 && !model.getStx(config).isLogHistogram(); Binding binding = xAxisRangeControl.getBindingContext().getBinding(PROPERTY_NAME_LOG_SCALED); if (!enabled) { binding.setPropertyValue(false); } log10AxisEnablement.apply(); binding.adjustComponents(); } private void updateXAxis() { final XYPlot plot = chart.getXYPlot(); plot.getDomainAxis().setLabel(getAxisLabel()); } private class AxisControlChangeListener implements PropertyChangeListener { boolean adjusting; @Override public void propertyChange(PropertyChangeEvent evt) { if (!adjusting) { adjusting = true; if (evt.getPropertyName().equals(PROPERTY_NAME_LOGARITHMIC_HISTOGRAM)) { if (evt.getNewValue().equals(Boolean.TRUE)) { xAxisRangeControl.adjustComponents(Stx.LOG10_SCALING.scale(xAxisRangeControl.getMin()), Stx.LOG10_SCALING.scale(xAxisRangeControl.getMax()), 3); } else { xAxisRangeControl.adjustComponents(Stx.LOG10_SCALING.scaleInverse(xAxisRangeControl.getMin()), Stx.LOG10_SCALING.scaleInverse(xAxisRangeControl.getMax()), 3); } } adjusting = false; } } } private class StxWorker extends ProgressMonitorSwingWorker<Stx, Object> { private final Double min; private final Double max; private final boolean autoMinMaxEnabled; private final boolean compute; public StxWorker(Double min, Double max, boolean autoMinMaxEnabled, boolean compute) { super(HistogramPanel.this, "Computing Histogram"); this.min = min; this.max = max; this.autoMinMaxEnabled = autoMinMaxEnabled; this.compute = compute; } @Override protected Stx doInBackground(ProgressMonitor pm) throws Exception { final Stx stx; HistogramPanelModel.HistogramConfig config = createHistogramConfig(); if (model.hasStx(config)) { return model.getStx(config); } if (!compute) { return null; } if (histogramPlotConfig.useRoiMask || histogramPlotConfig.numBins != Stx.DEFAULT_BIN_COUNT || histogramPlotConfig.histogramLogScaled || min != null || max != null) { final StxFactory factory = new StxFactory(); if (histogramPlotConfig.useRoiMask) { /*if(histogramPlotConfig.roiMask.getValidShape() == null){ handleError("The selected mask is empty.\n"+ "No valid histogram could be computed."); }*/ factory.withRoiMask(histogramPlotConfig.roiMask); } factory.withHistogramBinCount(histogramPlotConfig.numBins); factory.withLogHistogram(histogramPlotConfig.histogramLogScaled); if (min != null) { if (histogramPlotConfig.histogramLogScaled) { factory.withMinimum(Stx.LOG10_SCALING.scaleInverse(min)); } else { factory.withMinimum(min); } } if (max != null) { if (histogramPlotConfig.histogramLogScaled) { factory.withMaximum(Stx.LOG10_SCALING.scaleInverse(max)); } else { factory.withMaximum(max); } } stx = factory.create(getRaster(), pm); } else { stx = getRaster().getStx(true, pm); } if (getRaster() != config.raster) { return null; } return stx; } @Override public void done() { try { Stx stx = get(); if (stx == null) { return; } if (stx.getSampleCount() > 0) { if (autoMinMaxEnabled) { histogramComputing = true; xAxisRangeControl.adjustComponents( stx.getHistogramScaling().scale(stx.getMinimum()), stx.getHistogramScaling().scale(stx.getMaximum()), 4); histogramComputing = false; } setStx(stx); } else { Dialogs.showError("Either the selected ROI is empty or no pixels have been found within the minimum and maximum values specified.\n" + "No valid histogram could be computed.\n"); handleStxChange(); } } catch (ExecutionException e) { if (histogramPlotConfig.useRoiMask) { Dialogs.showError("An internal error occurred.\n" + "No valid histogram could be computed.\n" + "Possible reason: The selected ROI is empty."); } else { Dialogs.showError("An internal error occurred.\n" + "No valid histogram could be computed. Reason:\n" + e.getMessage()); } handleStxChange(); } catch (InterruptedException e) { Dialogs.showError("The histogram computation has been interrupted."); handleStxChange(); } } } private class ConfigChangeListener implements PropertyChangeListener, ActionListener { @Override public void propertyChange(PropertyChangeEvent evt) { handleConfigChanged(); } @Override public void actionPerformed(ActionEvent e) { handleConfigChanged(); } private void handleConfigChanged() { updateChartData(false); updateRefreshButton(); } } }