/* * 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.ValidationException; import com.bc.ceres.binding.ValueRange; import com.bc.ceres.swing.binding.BindingContext; import com.vividsolutions.jts.geom.Point; import org.esa.snap.core.datamodel.GeoCoding; import org.esa.snap.core.datamodel.GeoPos; import org.esa.snap.core.datamodel.Mask; import org.esa.snap.core.datamodel.PixelPos; import org.esa.snap.core.datamodel.Placemark; import org.esa.snap.core.datamodel.Product; import org.esa.snap.core.datamodel.ProductManager; import org.esa.snap.core.datamodel.ProductNodeEvent; import org.esa.snap.core.datamodel.RasterDataNode; import org.esa.snap.core.datamodel.VectorDataNode; import org.esa.snap.core.dataop.barithm.BandArithmetic; import org.esa.snap.core.util.SystemUtils; import org.esa.snap.core.util.math.MathUtils; import org.esa.snap.rcp.SnapApp; import org.esa.snap.rcp.util.Dialogs; import org.esa.snap.ui.GridBagUtils; import org.geotools.feature.FeatureCollection; import org.jfree.chart.ChartFactory; import org.jfree.chart.ChartPanel; import org.jfree.chart.JFreeChart; import org.jfree.chart.annotations.XYTitleAnnotation; import org.jfree.chart.axis.ValueAxis; import org.jfree.chart.block.BlockBorder; import org.jfree.chart.event.AxisChangeListener; import org.jfree.chart.plot.DatasetRenderingOrder; import org.jfree.chart.plot.PlotOrientation; import org.jfree.chart.plot.XYPlot; import org.jfree.chart.renderer.xy.DeviationRenderer; import org.jfree.chart.renderer.xy.XYErrorRenderer; import org.jfree.chart.title.TextTitle; import org.jfree.data.Range; import org.jfree.data.function.Function2D; import org.jfree.data.function.LineFunction2D; import org.jfree.data.general.DatasetUtilities; import org.jfree.data.statistics.Regression; import org.jfree.data.xy.XYDataItem; import org.jfree.data.xy.XYIntervalSeries; import org.jfree.data.xy.XYIntervalSeriesCollection; import org.jfree.data.xy.XYSeries; import org.jfree.ui.HorizontalAlignment; import org.jfree.ui.RectangleAnchor; import org.jfree.ui.RectangleEdge; import org.jfree.ui.RectangleInsets; import org.opengis.feature.simple.SimpleFeature; import org.opengis.feature.simple.SimpleFeatureType; import org.opengis.feature.type.AttributeDescriptor; import org.openide.windows.TopComponent; import javax.swing.JCheckBox; import javax.swing.JLabel; import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JSeparator; import javax.swing.JSpinner; import javax.swing.JTextField; import javax.swing.SwingUtilities; import javax.swing.SwingWorker; import javax.swing.table.DefaultTableModel; import javax.swing.table.TableModel; import java.awt.BasicStroke; import java.awt.BorderLayout; import java.awt.Color; import java.awt.Dimension; import java.awt.GridBagConstraints; import java.awt.Rectangle; import java.awt.Shape; import java.awt.geom.AffineTransform; import java.awt.geom.Point2D; import java.awt.geom.Rectangle2D; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.CancellationException; import java.util.concurrent.ExecutionException; import java.util.logging.Level; /** * The scatter plot pane within the statistics window. * * @author Olaf Danne * @author Sabine Embacher */ class ScatterPlotPanel extends ChartPagePanel { public static final String CHART_TITLE = "Correlative Plot"; private static final String NO_DATA_MESSAGE = "No correlative plot computed yet.\n" + "To create a correlative plot\n" + " -Select a band" + "\n" + " -Select vector data (e.g., a SeaDAS 6.x track)" + "\n" + " -Select the data as point data source" + "\n" + " -Select a data field" + "\n" + HELP_TIP_MESSAGE + "\n" + ZOOM_TIP_MESSAGE; private final String PROPERTY_NAME_X_AXIS_LOG_SCALED = "xAxisLogScaled"; private final String PROPERTY_NAME_Y_AXIS_LOG_SCALED = "yAxisLogScaled"; private final String PROPERTY_NAME_DATA_FIELD = "dataField"; private final String PROPERTY_NAME_POINT_DATA_SOURCE = "pointDataSource"; private final String PROPERTY_NAME_BOX_SIZE = "boxSize"; private final String PROPERTY_NAME_SHOW_ACCEPTABLE_DEVIATION = "showAcceptableDeviation"; private final String PROPERTY_NAME_ACCEPTABLE_DEVIATION = "acceptableDeviationInterval"; private final String PROPERTY_NAME_SHOW_REGRESSION_LINE = "showRegressionLine"; private final ScatterPlotModel scatterPlotModel; private final BindingContext bindingContext; private final AxisRangeControl xAxisRangeControl; private final AxisRangeControl yAxisRangeControl; private final XYIntervalSeriesCollection scatterpointsDataset; private final XYIntervalSeriesCollection acceptableDeviationDataset; private final XYIntervalSeriesCollection regressionDataset; private final JFreeChart chart; private ChartPanel scatterPlotDisplay; private ComputedData[] computedDatas; private CorrelativeFieldSelector correlativeFieldSelector; private Range xAutoRangeAxisRange; private Range yAutoRangeAxisRange; private AxisChangeListener domainAxisChangeListener; private boolean computingData; private XYTitleAnnotation r2Annotation; private final ProductManager.Listener productRemovedListener; private final Map<Product, UserSettings> userSettingsMap; ScatterPlotPanel(TopComponent parentDialog, String helpId) { super(parentDialog, helpId, CHART_TITLE, false); userSettingsMap = new HashMap<>(); productRemovedListener = new ProductManager.Listener() { @Override public void productAdded(ProductManager.Event event) { } @Override public void productRemoved(ProductManager.Event event) { final UserSettings userSettings = userSettingsMap.remove(event.getProduct()); if (userSettings != null) { userSettings.dispose(); } } }; xAxisRangeControl = new AxisRangeControl("X-Axis"); yAxisRangeControl = new AxisRangeControl("Y-Axis"); scatterPlotModel = new ScatterPlotModel(); bindingContext = new BindingContext(PropertyContainer.createObjectBacked(scatterPlotModel)); scatterpointsDataset = new XYIntervalSeriesCollection(); acceptableDeviationDataset = new XYIntervalSeriesCollection(); regressionDataset = new XYIntervalSeriesCollection(); r2Annotation = new XYTitleAnnotation(0, 0, new TextTitle("")); chart = ChartFactory.createScatterPlot(CHART_TITLE, "", "", scatterpointsDataset, PlotOrientation.VERTICAL, true, true, false); chart.getXYPlot().setDatasetRenderingOrder(DatasetRenderingOrder.FORWARD); createDomainAxisChangeListener(); final PropertyChangeListener userSettingsUpdateListener = evt -> { if (getRaster() != null) { final VectorDataNode pointDataSourceValue = scatterPlotModel.pointDataSource; final AttributeDescriptor dataFieldValue = scatterPlotModel.dataField; final UserSettings userSettings = getUserSettings(getRaster().getProduct()); userSettings.set(getRaster().getName(), pointDataSourceValue, dataFieldValue); } }; bindingContext.addPropertyChangeListener(PROPERTY_NAME_DATA_FIELD, userSettingsUpdateListener); bindingContext.addPropertyChangeListener(PROPERTY_NAME_POINT_DATA_SOURCE, userSettingsUpdateListener); } @Override protected void handleLayerContentChanged() { computeChartDataIfPossible(); } @Override protected String getDataAsText() { if (scatterpointsDataset.getItemCount(0) > 0) { final ScatterPlotTableModel scatterPlotTableModel; scatterPlotTableModel = new ScatterPlotTableModel(getRasterName(), getCorrelativeDataName(), computedDatas); return scatterPlotTableModel.toCVS(); } return ""; } @Override protected void initComponents() { getAlternativeView().initComponents(); initParameters(); createUI(); SnapApp.getDefault().getProductManager().addListener(productRemovedListener); } @Override protected void updateComponents() { super.updateComponents(); if (!isVisible()) { return; } final AttributeDescriptor dataField = scatterPlotModel.dataField; xAxisRangeControl.setTitleSuffix(dataField != null ? dataField.getLocalName() : null); final RasterDataNode raster = getRaster(); yAxisRangeControl.setTitleSuffix(raster != null ? raster.getName() : null); if (raster != null) { final Product product = getProduct(); final String rasterName = raster.getName(); final UserSettings userSettings = getUserSettings(product); final VectorDataNode userSelectedPointDataSource = userSettings.getPointDataSource(rasterName); final AttributeDescriptor userSelectedDataField = userSettings.getDataField(rasterName); correlativeFieldSelector.updatePointDataSource(product); correlativeFieldSelector.updateDataField(); if (userSelectedPointDataSource != null) { correlativeFieldSelector.tryToSelectPointDataSource(userSelectedPointDataSource); } if (userSelectedDataField != null) { correlativeFieldSelector.tryToSelectDataField(userSelectedDataField); } } if (isRasterChanged()) { getPlot().getRangeAxis().setLabel(StatisticChartStyling.getAxisLabel(raster, "X", false)); computeChartDataIfPossible(); } } private String getCorrelativeDataName() { return scatterPlotModel.dataField.getLocalName(); } @Override protected void updateChartData() { } @Override public void nodeAdded(ProductNodeEvent event) { if (event.getSourceNode() instanceof Placemark) { updateComponents(); } } @Override public void nodeRemoved(ProductNodeEvent event) { if (event.getSourceNode() instanceof VectorDataNode) { updateComponents(); computeChartDataIfPossible(); } } @Override protected void showAlternativeView() { final TableModel model; if (computedDatas != null && computedDatas.length > 0) { model = new ScatterPlotTableModel(getRasterName(), getCorrelativeDataName(), computedDatas); } else { model = new DefaultTableModel(); } final TableViewPagePanel alternativPanel = (TableViewPagePanel) getAlternativeView(); alternativPanel.setModel(model); super.showAlternativeView(); } private String getRasterName() { return getRaster() != null ? getRaster().getName() : ""; } private void initParameters() { final PropertyChangeListener recomputeListener = evt -> computeChartDataIfPossible(); bindingContext.addPropertyChangeListener(RoiMaskSelector.PROPERTY_NAME_USE_ROI_MASK, recomputeListener); bindingContext.addPropertyChangeListener(RoiMaskSelector.PROPERTY_NAME_ROI_MASK, recomputeListener); bindingContext.addPropertyChangeListener(PROPERTY_NAME_BOX_SIZE, recomputeListener); bindingContext.addPropertyChangeListener(PROPERTY_NAME_DATA_FIELD, recomputeListener); final PropertyChangeListener computeLineDataListener = evt -> computeRegressionAndAcceptableDeviationData(); bindingContext.addPropertyChangeListener(PROPERTY_NAME_SHOW_ACCEPTABLE_DEVIATION, computeLineDataListener); bindingContext.addPropertyChangeListener(PROPERTY_NAME_ACCEPTABLE_DEVIATION, computeLineDataListener); bindingContext.addPropertyChangeListener(PROPERTY_NAME_SHOW_REGRESSION_LINE, computeLineDataListener); final PropertyChangeListener rangeLabelUpdateListener = evt -> { final VectorDataNode pointDataSource = scatterPlotModel.pointDataSource; final AttributeDescriptor dataField = scatterPlotModel.dataField; if (dataField != null && pointDataSource != null) { final String dataFieldName = dataField.getLocalName(); getPlot().getDomainAxis().setLabel(dataFieldName); xAxisRangeControl.setTitleSuffix(dataFieldName); } else { getPlot().getDomainAxis().setLabel(""); xAxisRangeControl.setTitleSuffix(""); } }; bindingContext.addPropertyChangeListener(PROPERTY_NAME_DATA_FIELD, rangeLabelUpdateListener); bindingContext.addPropertyChangeListener(PROPERTY_NAME_POINT_DATA_SOURCE, rangeLabelUpdateListener); bindingContext.addPropertyChangeListener(PROPERTY_NAME_X_AXIS_LOG_SCALED, evt -> updateScalingOfXAxis()); bindingContext.addPropertyChangeListener(PROPERTY_NAME_Y_AXIS_LOG_SCALED, evt -> updateScalingOfYAxis()); xAxisRangeControl.getBindingContext().addPropertyChangeListener( evt -> handleAxisRangeControlChanges(evt, xAxisRangeControl, getPlot().getDomainAxis(), xAutoRangeAxisRange)); yAxisRangeControl.getBindingContext().addPropertyChangeListener( evt -> handleAxisRangeControlChanges(evt, yAxisRangeControl, getPlot().getRangeAxis(), yAutoRangeAxisRange)); } private void handleAxisRangeControlChanges(PropertyChangeEvent evt, AxisRangeControl axisRangeControl, ValueAxis valueAxis, Range computedAutoRange) { final String propertyName = evt.getPropertyName(); switch (propertyName) { case AxisRangeControl.PROPERTY_NAME_AUTO_MIN_MAX: if (axisRangeControl.isAutoMinMax()) { final double min = computedAutoRange.getLowerBound(); final double max = computedAutoRange.getUpperBound(); axisRangeControl.adjustComponents(min, max, 3); } break; case AxisRangeControl.PROPERTY_NAME_MIN: valueAxis.setLowerBound(axisRangeControl.getMin()); break; case AxisRangeControl.PROPERTY_NAME_MAX: valueAxis.setUpperBound(axisRangeControl.getMax()); break; } } private void createUI() { final XYPlot plot = getPlot(); plot.setAxisOffset(new RectangleInsets(5, 5, 5, 5)); plot.setNoDataMessage(NO_DATA_MESSAGE); int confidenceDSIndex = 0; int regressionDSIndex = 1; int scatterpointsDSIndex = 2; plot.setDataset(confidenceDSIndex, acceptableDeviationDataset); plot.setDataset(regressionDSIndex, regressionDataset); plot.setDataset(scatterpointsDSIndex, scatterpointsDataset); plot.addAnnotation(r2Annotation); final DeviationRenderer identityRenderer = new DeviationRenderer(true, false); identityRenderer.setSeriesPaint(0, StatisticChartStyling.SAMPLE_DATA_PAINT); identityRenderer.setSeriesFillPaint(0, StatisticChartStyling.SAMPLE_DATA_FILL_PAINT); plot.setRenderer(confidenceDSIndex, identityRenderer); final DeviationRenderer regressionRenderer = new DeviationRenderer(true, false); regressionRenderer.setSeriesPaint(0, StatisticChartStyling.REGRESSION_DATA_PAINT); regressionRenderer.setSeriesFillPaint(0, StatisticChartStyling.REGRESSION_DATA_FILL_PAINT); plot.setRenderer(regressionDSIndex, regressionRenderer); final XYErrorRenderer scatterPointsRenderer = new XYErrorRenderer(); scatterPointsRenderer.setDrawXError(true); scatterPointsRenderer.setErrorStroke(new BasicStroke(1)); scatterPointsRenderer.setErrorPaint(StatisticChartStyling.CORRELATIVE_POINT_OUTLINE_PAINT); scatterPointsRenderer.setSeriesShape(0, StatisticChartStyling.CORRELATIVE_POINT_SHAPE); scatterPointsRenderer.setSeriesOutlinePaint(0, StatisticChartStyling.CORRELATIVE_POINT_OUTLINE_PAINT); scatterPointsRenderer.setSeriesFillPaint(0, StatisticChartStyling.CORRELATIVE_POINT_FILL_PAINT); scatterPointsRenderer.setSeriesLinesVisible(0, false); scatterPointsRenderer.setSeriesShapesVisible(0, true); scatterPointsRenderer.setSeriesOutlineStroke(0, new BasicStroke(1.0f)); scatterPointsRenderer.setSeriesToolTipGenerator(0, (dataset, series, item) -> { final XYIntervalSeriesCollection collection = (XYIntervalSeriesCollection) dataset; final Comparable key = collection.getSeriesKey(series); final double xValue = collection.getXValue(series, item); final double endYValue = collection.getEndYValue(series, item); final double yValue = collection.getYValue(series, item); return String.format("%s: mean = %6.2f, sigma = %6.2f | %s: value = %6.2f", getRasterName(), yValue, endYValue - yValue, key, xValue); }); plot.setRenderer(scatterpointsDSIndex, scatterPointsRenderer); final boolean autoRangeIncludesZero = false; final boolean xLog = scatterPlotModel.xAxisLogScaled; final boolean yLog = scatterPlotModel.yAxisLogScaled; plot.setDomainAxis( StatisticChartStyling.updateScalingOfAxis(xLog, plot.getDomainAxis(), autoRangeIncludesZero)); plot.setRangeAxis(StatisticChartStyling.updateScalingOfAxis(yLog, plot.getRangeAxis(), autoRangeIncludesZero)); createUI(createChartPanel(chart), createInputParameterPanel(), bindingContext); plot.getDomainAxis().addChangeListener(domainAxisChangeListener); scatterPlotDisplay.setMouseWheelEnabled(true); scatterPlotDisplay.setMouseZoomable(true); } private void createDomainAxisChangeListener() { domainAxisChangeListener = event -> { if (!computingData) { computeRegressionAndAcceptableDeviationData(); } }; } private ChartPanel createChartPanel(final JFreeChart chart) { scatterPlotDisplay = new ChartPanel(chart) { @Override public void restoreAutoBounds() { // here we tweak the notify flag on the plot so that only // one notification happens even though we update multiple // axes... final XYPlot plot = chart.getXYPlot(); boolean savedNotify = plot.isNotify(); plot.setNotify(false); xAxisRangeControl.adjustAxis(plot.getDomainAxis(), 3); yAxisRangeControl.adjustAxis(plot.getRangeAxis(), 3); plot.setNotify(savedNotify); } }; MaskSelectionToolSupport maskSelectionToolSupport = new MaskSelectionToolSupport(this, scatterPlotDisplay, "correlative_plot_area", "Mask generated from selected correlative plot area", Color.RED, PlotAreaSelectionTool.AreaType.Y_RANGE) { @Override protected String createMaskExpression(PlotAreaSelectionTool.AreaType areaType, Shape shape) { Rectangle2D bounds = shape.getBounds2D(); return createMaskExpression(bounds.getMinY(), bounds.getMaxY()); } protected String createMaskExpression(double x1, double x2) { String bandName = BandArithmetic.createExternalName(getRaster().getName()); return String.format("%s >= %s && %s <= %s", bandName, x1, bandName, x2); } }; scatterPlotDisplay.getPopupMenu().addSeparator(); scatterPlotDisplay.getPopupMenu().add(maskSelectionToolSupport.createMaskSelectionModeMenuItem()); scatterPlotDisplay.getPopupMenu().add(maskSelectionToolSupport.createDeleteMaskMenuItem()); scatterPlotDisplay.getPopupMenu().addSeparator(); scatterPlotDisplay.getPopupMenu().add(createCopyDataToClipboardMenuItem()); return scatterPlotDisplay; } private JPanel createInputParameterPanel() { final PropertyDescriptor boxSizeDescriptor = bindingContext.getPropertySet().getDescriptor( PROPERTY_NAME_BOX_SIZE); boxSizeDescriptor.setValueRange(new ValueRange(1, 101)); boxSizeDescriptor.setAttribute("stepSize", 2); boxSizeDescriptor.setValidator((property, value) -> { if (((Number) value).intValue() % 2 == 0) { throw new ValidationException("Only odd values allowed as box size."); } }); final JSpinner boxSizeSpinner = new JSpinner(); bindingContext.bind(PROPERTY_NAME_BOX_SIZE, boxSizeSpinner); final JPanel boxSizePanel = new JPanel(new BorderLayout(5, 3)); boxSizePanel.add(new JLabel("Box size:"), BorderLayout.WEST); boxSizePanel.add(boxSizeSpinner); correlativeFieldSelector = new CorrelativeFieldSelector(bindingContext); final JPanel pointDataSourcePanel = new JPanel(new BorderLayout(5, 3)); pointDataSourcePanel.add(correlativeFieldSelector.pointDataSourceLabel, BorderLayout.NORTH); pointDataSourcePanel.add(correlativeFieldSelector.pointDataSourceList); final JPanel pointDataFieldPanel = new JPanel(new BorderLayout(5, 3)); pointDataFieldPanel.add(correlativeFieldSelector.dataFieldLabel, BorderLayout.NORTH); pointDataFieldPanel.add(correlativeFieldSelector.dataFieldList); final JCheckBox xLogCheck = new JCheckBox("Log10 scaled"); bindingContext.bind(PROPERTY_NAME_X_AXIS_LOG_SCALED, xLogCheck); final JPanel xAxisOptionPanel = new JPanel(new BorderLayout()); xAxisOptionPanel.add(xAxisRangeControl.getPanel()); xAxisOptionPanel.add(xLogCheck, BorderLayout.SOUTH); final JCheckBox yLogCheck = new JCheckBox("Log10 scaled"); bindingContext.bind(PROPERTY_NAME_Y_AXIS_LOG_SCALED, yLogCheck); final JPanel yAxisOptionPanel = new JPanel(new BorderLayout()); yAxisOptionPanel.add(yAxisRangeControl.getPanel()); yAxisOptionPanel.add(yLogCheck, BorderLayout.SOUTH); final JCheckBox acceptableCheck = new JCheckBox("Show tolerance range"); JLabel fieldPrefix = new JLabel("+/-"); final JTextField acceptableField = new JTextField(); acceptableField.setPreferredSize(new Dimension(40, acceptableField.getPreferredSize().height)); acceptableField.setHorizontalAlignment(JTextField.RIGHT); final JLabel percentLabel = new JLabel(" %"); bindingContext.bind(PROPERTY_NAME_SHOW_ACCEPTABLE_DEVIATION, acceptableCheck); bindingContext.bind(PROPERTY_NAME_ACCEPTABLE_DEVIATION, acceptableField); bindingContext.getBinding(PROPERTY_NAME_ACCEPTABLE_DEVIATION).addComponent(percentLabel); bindingContext.getBinding(PROPERTY_NAME_ACCEPTABLE_DEVIATION).addComponent(fieldPrefix); bindingContext.bindEnabledState(PROPERTY_NAME_ACCEPTABLE_DEVIATION, true, PROPERTY_NAME_SHOW_ACCEPTABLE_DEVIATION, true); final JPanel confidencePanel = GridBagUtils.createPanel(); GridBagConstraints confidencePanelConstraints = GridBagUtils.createConstraints( "anchor=NORTHWEST,fill=HORIZONTAL,insets.top=5,weighty=0,weightx=1"); GridBagUtils.addToPanel(confidencePanel, acceptableCheck, confidencePanelConstraints, "gridy=0,gridwidth=3"); GridBagUtils.addToPanel(confidencePanel, fieldPrefix, confidencePanelConstraints, "weightx=0,insets.left=22,gridy=1,gridx=0,insets.top=4,gridwidth=1"); GridBagUtils.addToPanel(confidencePanel, acceptableField, confidencePanelConstraints, "weightx=1,gridx=1,insets.left=2,insets.top=2"); GridBagUtils.addToPanel(confidencePanel, percentLabel, confidencePanelConstraints, "weightx=0,gridx=2,insets.left=0,insets.top=4"); final JCheckBox regressionCheck = new JCheckBox("Show regression line"); bindingContext.bind(PROPERTY_NAME_SHOW_REGRESSION_LINE, regressionCheck); // UI arrangement JPanel middlePanel = GridBagUtils.createPanel(); GridBagConstraints middlePanelConstraints = GridBagUtils.createConstraints( "anchor=NORTHWEST,fill=HORIZONTAL,insets.top=6,weighty=0,weightx=1"); GridBagUtils.addToPanel(middlePanel, boxSizePanel, middlePanelConstraints, "gridy=0,insets.left=6"); GridBagUtils.addToPanel(middlePanel, pointDataSourcePanel, middlePanelConstraints, "gridy=1"); GridBagUtils.addToPanel(middlePanel, pointDataFieldPanel, middlePanelConstraints, "gridy=2"); GridBagUtils.addToPanel(middlePanel, xAxisOptionPanel, middlePanelConstraints, "gridy=3,insets.left=0"); GridBagUtils.addToPanel(middlePanel, yAxisOptionPanel, middlePanelConstraints, "gridy=4"); GridBagUtils.addToPanel(middlePanel, new JSeparator(), middlePanelConstraints, "gridy=5,insets.left=4"); GridBagUtils.addToPanel(middlePanel, confidencePanel, middlePanelConstraints, "gridy=6,fill=HORIZONTAL,insets.left=-4"); GridBagUtils.addToPanel(middlePanel, regressionCheck, middlePanelConstraints, "gridy=7,insets.left=-4,insets.top=8"); return middlePanel; } private void updateScalingOfXAxis() { final boolean logScaled = scatterPlotModel.xAxisLogScaled; final ValueAxis oldAxis = getPlot().getDomainAxis(); ValueAxis newAxis = StatisticChartStyling.updateScalingOfAxis(logScaled, oldAxis, false); oldAxis.removeChangeListener(domainAxisChangeListener); newAxis.addChangeListener(domainAxisChangeListener); getPlot().setDomainAxis(newAxis); finishScalingUpdate(xAxisRangeControl, newAxis, oldAxis); } private void updateScalingOfYAxis() { final boolean logScaled = scatterPlotModel.yAxisLogScaled; final ValueAxis oldAxis = getPlot().getRangeAxis(); ValueAxis newAxis = StatisticChartStyling.updateScalingOfAxis(logScaled, oldAxis, false); getPlot().setRangeAxis(newAxis); finishScalingUpdate(yAxisRangeControl, newAxis, oldAxis); } private void finishScalingUpdate(AxisRangeControl axisRangeControl, ValueAxis newAxis, ValueAxis oldAxis) { if (axisRangeControl.isAutoMinMax()) { newAxis.setAutoRange(false); acceptableDeviationDataset.removeAllSeries(); regressionDataset.removeAllSeries(); getPlot().removeAnnotation(r2Annotation); newAxis.setAutoRange(true); axisRangeControl.adjustComponents(newAxis, 3); newAxis.setAutoRange(false); computeRegressionAndAcceptableDeviationData(); } else { newAxis.setAutoRange(false); newAxis.setRange(oldAxis.getRange()); } } private XYPlot getPlot() { return chart.getXYPlot(); } private void computeChartDataIfPossible() { // need to do this later: all GUI events must be processed first in order to get the correct state SwingUtilities.invokeLater(() -> { if (scatterPlotModel.pointDataSource != null && scatterPlotModel.dataField != null && scatterPlotModel.pointDataSource.getFeatureCollection() != null && scatterPlotModel.pointDataSource.getFeatureCollection().features() != null && scatterPlotModel.pointDataSource.getFeatureCollection().features().hasNext() && scatterPlotModel.pointDataSource.getFeatureCollection().features().next() != null && scatterPlotModel.pointDataSource.getFeatureCollection().features().next().getAttribute( scatterPlotModel.dataField.getLocalName()) != null && getRaster() != null) { compute(scatterPlotModel.useRoiMask ? scatterPlotModel.roiMask : null); } else { scatterpointsDataset.removeAllSeries(); acceptableDeviationDataset.removeAllSeries(); regressionDataset.removeAllSeries(); getPlot().removeAnnotation(r2Annotation); computedDatas = null; } }); } private void compute(final Mask selectedMask) { final RasterDataNode raster = getRaster(); final AttributeDescriptor dataField = scatterPlotModel.dataField; if (raster == null || dataField == null) { return; } SwingWorker<ComputedData[], Object> swingWorker = new SwingWorker<ComputedData[], Object>() { @Override protected ComputedData[] doInBackground() throws Exception { SystemUtils.LOG.finest("start computing scatter plot data"); final List<ComputedData> computedDataList = new ArrayList<>(); final FeatureCollection<SimpleFeatureType, SimpleFeature> collection = scatterPlotModel.pointDataSource.getFeatureCollection(); final SimpleFeature[] features = collection.toArray(new SimpleFeature[collection.size()]); final int boxSize = scatterPlotModel.boxSize; final Rectangle sceneRect = new Rectangle(raster.getRasterWidth(), raster.getRasterHeight()); final GeoCoding geoCoding = raster.getGeoCoding(); final AffineTransform imageToModelTransform; imageToModelTransform = Product.findImageToModelTransform(geoCoding); for (SimpleFeature feature : features) { final Point point = (Point) feature.getDefaultGeometryProperty().getValue(); Point2D modelPos = new Point2D.Float((float) point.getX(), (float) point.getY()); final Point2D imagePos = imageToModelTransform.inverseTransform(modelPos, null); if (!sceneRect.contains(imagePos)) { continue; } final float imagePosX = (float) imagePos.getX(); final float imagePosY = (float) imagePos.getY(); final Rectangle imageRect = sceneRect.intersection(new Rectangle(((int) imagePosX) - boxSize / 2, ((int) imagePosY) - boxSize / 2, boxSize, boxSize)); if (imageRect.isEmpty()) { continue; } final double[] rasterValues = new double[imageRect.width * imageRect.height]; raster.readPixels(imageRect.x, imageRect.y, imageRect.width, imageRect.height, rasterValues); final int[] maskBuffer = new int[imageRect.width * imageRect.height]; Arrays.fill(maskBuffer, 1); if (selectedMask != null) { selectedMask.readPixels(imageRect.x, imageRect.y, imageRect.width, imageRect.height, maskBuffer); } final int centerIndex = imageRect.width * (imageRect.height / 2) + (imageRect.width / 2); if (maskBuffer[centerIndex] == 0) { continue; } double sum = 0; double sumSqr = 0; int n = 0; boolean valid = false; for (int y = 0; y < imageRect.height; y++) { for (int x = 0; x < imageRect.width; x++) { final int index = y * imageRect.height + x; if (raster.isPixelValid(x + imageRect.x, y + imageRect.y) && maskBuffer[index] != 0) { final double rasterValue = rasterValues[index]; sum += rasterValue; sumSqr += rasterValue * rasterValue; n++; valid = true; } } } if (!valid) { continue; } double rasterMean = sum / n; double rasterSigma = n > 1 ? Math.sqrt((sumSqr - (sum * sum) / n) / (n - 1)) : 0.0; String localName = dataField.getLocalName(); Number attribute = (Number) feature.getAttribute(localName); final Collection<org.opengis.feature.Property> featureProperties = feature.getProperties(); final float correlativeData = attribute.floatValue(); final GeoPos geoPos = new GeoPos(); if (geoCoding.canGetGeoPos()) { final PixelPos pixelPos = new PixelPos(imagePosX, imagePosY); geoCoding.getGeoPos(pixelPos, geoPos); } else { geoPos.setInvalid(); } computedDataList.add( new ComputedData(imagePosX, imagePosY, (float) geoPos.getLat(), (float) geoPos.getLon(), (float) rasterMean, (float) rasterSigma, correlativeData, featureProperties)); } return computedDataList.toArray(new ComputedData[computedDataList.size()]); } @Override public void done() { try { final ValueAxis xAxis = getPlot().getDomainAxis(); final ValueAxis yAxis = getPlot().getRangeAxis(); xAxis.setAutoRange(false); yAxis.setAutoRange(false); scatterpointsDataset.removeAllSeries(); acceptableDeviationDataset.removeAllSeries(); regressionDataset.removeAllSeries(); getPlot().removeAnnotation(r2Annotation); computedDatas = null; final ComputedData[] data = get(); if (data.length == 0) { return; } computedDatas = data; final XYIntervalSeries scatterValues = new XYIntervalSeries(getCorrelativeDataName()); for (ComputedData computedData : computedDatas) { final float rasterMean = computedData.rasterMean; final float rasterSigma = computedData.rasterSigma; final float correlativeData = computedData.correlativeData; scatterValues.add(correlativeData, correlativeData, correlativeData, rasterMean, rasterMean - rasterSigma, rasterMean + rasterSigma); } computingData = true; scatterpointsDataset.addSeries(scatterValues); xAxis.setAutoRange(true); yAxis.setAutoRange(true); xAxis.setAutoRange(false); yAxis.setAutoRange(false); xAutoRangeAxisRange = new Range(xAxis.getLowerBound(), xAxis.getUpperBound()); yAutoRangeAxisRange = new Range(yAxis.getLowerBound(), yAxis.getUpperBound()); if (xAxisRangeControl.isAutoMinMax()) { xAxisRangeControl.adjustComponents(xAxis, 3); } else { xAxisRangeControl.adjustAxis(xAxis, 3); } if (yAxisRangeControl.isAutoMinMax()) { yAxisRangeControl.adjustComponents(yAxis, 3); } else { yAxisRangeControl.adjustAxis(yAxis, 3); } computeRegressionAndAcceptableDeviationData(); computingData = false; } catch (InterruptedException | CancellationException e) { SystemUtils.LOG.log(Level.WARNING, "Failed to compute correlative plot.", e); Dialogs.showMessage(CHART_TITLE, "Failed to compute correlative plot.\n" + "Calculation canceled.", JOptionPane.ERROR_MESSAGE, null); } catch (ExecutionException e) { SystemUtils.LOG.log(Level.WARNING, "Failed to compute correlative plot.", e); Dialogs.showMessage(CHART_TITLE, "Failed to compute correlative plot.\n" + "An error occurred:\n" + e.getCause().getMessage(), JOptionPane.ERROR_MESSAGE, null); } } }; swingWorker.execute(); } private void computeRegressionAndAcceptableDeviationData() { acceptableDeviationDataset.removeAllSeries(); regressionDataset.removeAllSeries(); getPlot().removeAnnotation(r2Annotation); if (computedDatas != null) { final ValueAxis domainAxis = getPlot().getDomainAxis(); final double min = domainAxis.getLowerBound(); final double max = domainAxis.getUpperBound(); acceptableDeviationDataset.addSeries(computeAcceptableDeviationData(min, max)); if (scatterPlotModel.showRegressionLine) { final XYIntervalSeries series = computeRegressionData(min, max); if (series != null) { regressionDataset.addSeries(series); computeCoefficientOfDetermination(); } } } } private XYIntervalSeries computeRegressionData(double xStart, double xEnd) { if (scatterpointsDataset.getItemCount(0) > 1) { final double[] coefficients = Regression.getOLSRegression(scatterpointsDataset, 0); final Function2D curve = new LineFunction2D(coefficients[0], coefficients[1]); final XYSeries regressionData = DatasetUtilities.sampleFunction2DToSeries(curve, xStart, xEnd, 100, "regression line"); final XYIntervalSeries xyIntervalRegression = new XYIntervalSeries(regressionData.getKey()); for (int i = 0; i < regressionData.getItemCount(); i++) { XYDataItem item = regressionData.getDataItem(i); final double x = item.getXValue(); final double y = item.getYValue(); xyIntervalRegression.add(x, x, x, y, y, y); } return xyIntervalRegression; } else { Dialogs.showInformation("Unable to compute regression line.\n" + "At least 2 values are needed to compute regression coefficients."); return null; } } private void computeCoefficientOfDetermination() { int numberOfItems = scatterpointsDataset.getSeries(0).getItemCount(); double arithmeticMeanOfX = 0; //arithmetic mean of X double arithmeticMeanOfY = 0; //arithmetic mean of Y double varX = 0; //variance of X double varY = 0; //variance of Y double coVarXY = 0; //covariance of X and Y; //compute arithmetic means for (int i = 0; i < numberOfItems; i++) { arithmeticMeanOfX += scatterpointsDataset.getXValue(0, i); arithmeticMeanOfY += scatterpointsDataset.getYValue(0, i); } arithmeticMeanOfX /= numberOfItems; arithmeticMeanOfY /= numberOfItems; //compute variances and covariance for (int i = 0; i < numberOfItems; i++) { varX += Math.pow(scatterpointsDataset.getXValue(0, i) - arithmeticMeanOfX, 2); varY += Math.pow(scatterpointsDataset.getYValue(0, i) - arithmeticMeanOfY, 2); coVarXY += (scatterpointsDataset.getXValue(0, i) - arithmeticMeanOfX) * (scatterpointsDataset.getYValue(0, i) - arithmeticMeanOfY); } //computation of coefficient of determination double r2 = Math.pow(coVarXY, 2) / (varX * varY); r2 = MathUtils.round(r2, Math.pow(10.0, 5)); final double[] coefficients = Regression.getOLSRegression(scatterpointsDataset, 0); final double intercept = coefficients[0]; final double slope = coefficients[1]; final String linearEquation; if (intercept >= 0) { linearEquation = "y = " + (float) slope + "x + " + (float) intercept; } else { linearEquation = "y = " + (float) slope + "x - " + Math.abs((float) intercept); } TextTitle tt = new TextTitle(linearEquation + "\nRĀ² = " + r2); tt.setTextAlignment(HorizontalAlignment.RIGHT); tt.setFont(chart.getLegend().getItemFont()); tt.setBackgroundPaint(new Color(200, 200, 255, 100)); tt.setFrame(new BlockBorder(Color.white)); tt.setPosition(RectangleEdge.BOTTOM); r2Annotation = new XYTitleAnnotation(0.98, 0.02, tt, RectangleAnchor.BOTTOM_RIGHT); r2Annotation.setMaxWidth(0.48); getPlot().addAnnotation(r2Annotation); } private XYIntervalSeries computeAcceptableDeviationData(double lowerBound, double upperBound) { final XYSeries identity = DatasetUtilities.sampleFunction2DToSeries(x -> x, lowerBound, upperBound, 100, "1:1 line"); final XYIntervalSeries xyIntervalSeries = new XYIntervalSeries(identity.getKey()); for (int i = 0; i < identity.getItemCount(); i++) { XYDataItem item = identity.getDataItem(i); final double x = item.getXValue(); final double y = item.getYValue(); if (scatterPlotModel.showAcceptableDeviation) { final double acceptableDeviation = scatterPlotModel.acceptableDeviationInterval; final double xOff = acceptableDeviation * x / 100; final double yOff = acceptableDeviation * y / 100; xyIntervalSeries.add(x, x - xOff, x + xOff, y, y - yOff, y + yOff); } else { xyIntervalSeries.add(x, x, x, y, y, y); } } return xyIntervalSeries; } // The fields of this class are used by the binding framework @SuppressWarnings("UnusedDeclaration") static class ScatterPlotModel { private int boxSize = 1; private boolean useRoiMask; private Mask roiMask; private VectorDataNode pointDataSource; private AttributeDescriptor dataField; private boolean xAxisLogScaled; private boolean yAxisLogScaled; private boolean showAcceptableDeviation; private double acceptableDeviationInterval = 15; public boolean showRegressionLine; } static class ComputedData { final float x; final float y; final float lat; final float lon; final float rasterMean; final float rasterSigma; final float correlativeData; final Collection<org.opengis.feature.Property> featureProperties; ComputedData(float x, float y, float lat, float lon, float rasterMean, float rasterSigma, float correlativeData, Collection<org.opengis.feature.Property> featureProperties) { this.x = x; this.y = y; this.lat = lat; this.lon = lon; this.rasterMean = rasterMean; this.rasterSigma = rasterSigma; this.correlativeData = correlativeData; this.featureProperties = featureProperties; } } private UserSettings getUserSettings(Product product) { if (product == null) { return null; } if (userSettingsMap.get(product) == null) { userSettingsMap.put(product, new UserSettings()); } return userSettingsMap.get(product); } private static class UserSettings { Map<String, VectorDataNode> pointDataSource = new HashMap<>(); Map<String, AttributeDescriptor> dataField = new HashMap<>(); public void set(String rasterName, VectorDataNode pointDataSourceValue, AttributeDescriptor dataFieldValue) { if (pointDataSourceValue != null && dataFieldValue != null) { pointDataSource.put(rasterName, pointDataSourceValue); dataField.put(rasterName, dataFieldValue); } } public VectorDataNode getPointDataSource(String rasterName) { return pointDataSource.get(rasterName); } public AttributeDescriptor getDataField(String rasterName) { return dataField.get(rasterName); } public void dispose() { pointDataSource.clear(); pointDataSource = null; dataField.clear(); dataField = null; } } }