/* * 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.Property; import com.bc.ceres.binding.PropertyContainer; import com.bc.ceres.binding.PropertyDescriptor; import com.bc.ceres.binding.ValidationException; import com.bc.ceres.binding.Validator; import com.bc.ceres.binding.ValueRange; import com.bc.ceres.swing.binding.BindingContext; import com.bc.ceres.swing.binding.Enablement; import org.esa.snap.core.datamodel.GeoPos; import org.esa.snap.core.datamodel.Mask; import org.esa.snap.core.datamodel.ProductNodeEvent; import org.esa.snap.core.datamodel.RasterDataNode; import org.esa.snap.core.datamodel.TransectProfileData; import org.esa.snap.core.datamodel.TransectProfileDataBuilder; import org.esa.snap.core.datamodel.VectorDataNode; import org.esa.snap.core.dataop.barithm.BandArithmetic; import org.esa.snap.rcp.sync.DefaultCursorSynchronizer; import org.esa.snap.ui.AbstractDialog; 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.event.AxisChangeEvent; import org.jfree.chart.event.AxisChangeListener; import org.jfree.chart.plot.IntervalMarker; 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.renderer.xy.XYLineAndShapeRenderer; import org.jfree.data.xy.XYDataset; import org.jfree.data.xy.XYIntervalSeries; import org.jfree.data.xy.XYIntervalSeriesCollection; import org.jfree.ui.Layer; import org.jfree.ui.RectangleInsets; import org.opengis.feature.simple.SimpleFeature; import org.opengis.feature.type.AttributeDescriptor; import org.openide.windows.TopComponent; import javax.swing.JCheckBox; import javax.swing.JLabel; import javax.swing.JPanel; import javax.swing.JSpinner; import javax.swing.table.DefaultTableModel; import javax.swing.table.TableModel; import java.awt.BasicStroke; import java.awt.Color; import java.awt.GridBagConstraints; import java.awt.Shape; import java.awt.geom.Point2D; import java.awt.geom.Rectangle2D; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.io.IOException; import java.util.HashSet; import java.util.Set; /** * The profile plot pane within the statistics window. * * @author Norman Fomferra * @author Thomas Storm */ class ProfilePlotPanel extends ChartPagePanel { public static final String CHART_TITLE = "Profile Plot"; private static final String NO_DATA_MESSAGE = "No profile plot computed yet.\n" + "It will be computed if vector data (a polygon, an ellipse, or a line)\n" + "is selected within the image view.\n" + HELP_TIP_MESSAGE + "\n" + ZOOM_TIP_MESSAGE; public static final String PROPERTY_NAME_MARK_SEGMENTS = "markSegments"; public static final String PROPERTY_NAME_LOG_SCALED = "logScaled"; public static final String DEFAULT_SAMPLE_DATASET_NAME = "Sample"; private AxisRangeControl xAxisRangeControl; private AxisRangeControl yAxisRangeControl; private boolean isInitialized; private ChartPanel profilePlotDisplay; private JFreeChart chart; private XYIntervalSeriesCollection dataset; private TransectProfileData profileData; private boolean axisAdjusting = false; private Set<IntervalMarker> intervalMarkers; private CorrelativeFieldSelector correlativeFieldSelector; private DataSourceConfig dataSourceConfig; private DeviationRenderer deviationRenderer; private XYErrorRenderer pointRenderer; private Enablement pointDataSourceEnablement; private Enablement dataFieldEnablement; private DefaultCursorSynchronizer cursorSynchronizer; ProfilePlotPanel(TopComponent parentComponent, String helpId) { super(parentComponent, helpId, CHART_TITLE, false); } private ChartPanel createChartPanel(JFreeChart chart) { profilePlotDisplay = new ChartPanel(chart); MaskSelectionToolSupport maskSelectionToolSupport = new MaskSelectionToolSupport(this, profilePlotDisplay, "profile_plot_area", "Mask generated from selected profile 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); } }; profilePlotDisplay.addChartMouseListener(new XYPlotMarker(profilePlotDisplay, new XYPlotMarker.Listener() { @Override public void pointSelected(XYDataset xyDataset, int seriesIndex, Point2D dataPoint) { if (profileData != null) { GeoPos[] geoPositions = profileData.getGeoPositions(); int index = (int) dataPoint.getX(); if (index >= 0 && index < geoPositions.length) { if (cursorSynchronizer == null) { cursorSynchronizer = new DefaultCursorSynchronizer(); } if (!cursorSynchronizer.isEnabled()) { cursorSynchronizer.setEnabled(true); } cursorSynchronizer.updateCursorOverlays(geoPositions[index]); } } } @Override public void pointDeselected() { cursorSynchronizer.setEnabled(false); } })); profilePlotDisplay.setInitialDelay(200); profilePlotDisplay.setDismissDelay(1500); profilePlotDisplay.setReshowDelay(200); profilePlotDisplay.setZoomTriggerDistance(5); profilePlotDisplay.getPopupMenu().addSeparator(); profilePlotDisplay.getPopupMenu().add(maskSelectionToolSupport.createMaskSelectionModeMenuItem()); profilePlotDisplay.getPopupMenu().add(maskSelectionToolSupport.createDeleteMaskMenuItem()); profilePlotDisplay.getPopupMenu().addSeparator(); profilePlotDisplay.getPopupMenu().add(createCopyDataToClipboardMenuItem()); return profilePlotDisplay; } @Override protected void showAlternativeView() { final TableModel model; if (profileData != null) { model = createProfileDataTableModel(); } else { model = new DefaultTableModel(); } final TableViewPagePanel alternativePanel = (TableViewPagePanel) getAlternativeView(); alternativePanel.setModel(model); super.showAlternativeView(); } @Override protected void initComponents() { getAlternativeView().initComponents(); dataset = new XYIntervalSeriesCollection(); this.chart = ChartFactory.createXYLineChart( CHART_TITLE, "Path in pixels", DEFAULT_SAMPLE_DATASET_NAME, dataset, PlotOrientation.VERTICAL, true, true, false ); final XYPlot plot = chart.getXYPlot(); deviationRenderer = new DeviationRenderer(); deviationRenderer.setUseFillPaint(true); deviationRenderer.setBaseToolTipGenerator(new XYPlotToolTipGenerator()); deviationRenderer.setSeriesLinesVisible(0, true); deviationRenderer.setSeriesShapesVisible(0, false); deviationRenderer.setSeriesStroke(0, new BasicStroke(1.0f)); deviationRenderer.setSeriesPaint(0, StatisticChartStyling.SAMPLE_DATA_PAINT); deviationRenderer.setSeriesFillPaint(0, StatisticChartStyling.SAMPLE_DATA_FILL_PAINT); pointRenderer = new XYErrorRenderer(); pointRenderer.setUseFillPaint(true); pointRenderer.setBaseToolTipGenerator(new XYPlotToolTipGenerator()); pointRenderer.setSeriesLinesVisible(0, false); pointRenderer.setSeriesShapesVisible(0, true); pointRenderer.setSeriesStroke(0, new BasicStroke(1.0f)); pointRenderer.setSeriesPaint(0, StatisticChartStyling.SAMPLE_DATA_PAINT); pointRenderer.setSeriesFillPaint(0, StatisticChartStyling.SAMPLE_DATA_FILL_PAINT); pointRenderer.setSeriesShape(0, StatisticChartStyling.SAMPLE_DATA_POINT_SHAPE); configureRendererForCorrelativeData(deviationRenderer); configureRendererForCorrelativeData(pointRenderer); plot.setNoDataMessage(NO_DATA_MESSAGE); plot.setAxisOffset(new RectangleInsets(5, 5, 5, 5)); plot.setRenderer(deviationRenderer); final AxisChangeListener axisListener = new AxisChangeListener() { @Override public void axisChanged(AxisChangeEvent event) { adjustAxisControlComponents(); } }; final ValueAxis domainAxis = plot.getDomainAxis(); final ValueAxis rangeAxis = plot.getRangeAxis(); // allow transfer from bounds into min/max fields, if auto min/maxis enabled domainAxis.setAutoRange(true); rangeAxis.setAutoRange(true); domainAxis.addChangeListener(axisListener); rangeAxis.addChangeListener(axisListener); intervalMarkers = new HashSet<>(); xAxisRangeControl = new AxisRangeControl("X-Axis"); yAxisRangeControl = new AxisRangeControl("Y-Axis"); final PropertyChangeListener changeListener = new PropertyChangeListener() { @Override public void propertyChange(PropertyChangeEvent evt) { if (evt.getPropertyName().equals(PROPERTY_NAME_MARK_SEGMENTS)) { updateDataSet(); } if (evt.getPropertyName().equals(PROPERTY_NAME_LOG_SCALED)) { updateScalingOfYAxis(); } updateUIState(); } }; xAxisRangeControl.getBindingContext().addPropertyChangeListener(changeListener); xAxisRangeControl.getBindingContext().getPropertySet().addProperty(Property.create(PROPERTY_NAME_MARK_SEGMENTS, false)); xAxisRangeControl.getBindingContext().getPropertySet().getDescriptor(PROPERTY_NAME_MARK_SEGMENTS).setDescription("Toggle whether to mark segments"); yAxisRangeControl.getBindingContext().addPropertyChangeListener(changeListener); yAxisRangeControl.getBindingContext().getPropertySet().addProperty(Property.create(PROPERTY_NAME_LOG_SCALED, false)); yAxisRangeControl.getBindingContext().getPropertySet().getDescriptor(PROPERTY_NAME_LOG_SCALED).setDescription("Toggle whether to use a logarithmic axis"); dataSourceConfig = new DataSourceConfig(); final BindingContext bindingContext = new BindingContext(PropertyContainer.createObjectBacked(dataSourceConfig)); JPanel middlePanel = createMiddlePanel(bindingContext); createUI(createChartPanel(chart), middlePanel, bindingContext); isInitialized = true; updateComponents(); } protected JPanel createMiddlePanel(BindingContext bindingContext) { final JLabel boxSizeLabel = new JLabel("Box size: "); final JSpinner boxSizeSpinner = new JSpinner(); final JCheckBox computeInBetweenPoints = new JCheckBox("Compute in-between points"); final JCheckBox useCorrelativeData = new JCheckBox("Use correlative data"); correlativeFieldSelector = new CorrelativeFieldSelector(bindingContext); final PropertyDescriptor boxSizeDescriptor = bindingContext.getPropertySet().getProperty("boxSize").getDescriptor(); boxSizeDescriptor.setValueRange(new ValueRange(1, 101)); boxSizeDescriptor.setAttribute("stepSize", 2); boxSizeDescriptor.setValidator(new Validator() { @Override public void validateValue(Property property, Object value) throws ValidationException { if (((Number) value).intValue() % 2 == 0) { throw new ValidationException("Only odd values allowed as box size."); } } }); bindingContext.bind("boxSize", boxSizeSpinner); bindingContext.bind("computeInBetweenPoints", computeInBetweenPoints); bindingContext.bind("useCorrelativeData", useCorrelativeData); EnablePointDataCondition condition = new EnablePointDataCondition(); pointDataSourceEnablement = bindingContext.bindEnabledState("pointDataSource", true, condition); dataFieldEnablement = bindingContext.bindEnabledState("dataField", true, condition); bindingContext.addPropertyChangeListener(new PropertyChangeListener() { @Override public void propertyChange(PropertyChangeEvent evt) { updateDataSource(); updateDataSet(); updateUIState(); } }); JPanel dataSourceOptionsPanel = GridBagUtils.createPanel(); GridBagConstraints dataSourceOptionsConstraints = GridBagUtils.createConstraints("anchor=NORTHWEST,fill=HORIZONTAL,insets.top=2"); GridBagUtils.addToPanel(dataSourceOptionsPanel, boxSizeLabel, dataSourceOptionsConstraints, "gridwidth=1,gridy=0,gridx=0,weightx=0,insets.left=4"); GridBagUtils.addToPanel(dataSourceOptionsPanel, boxSizeSpinner, dataSourceOptionsConstraints, "gridwidth=1,gridy=0,gridx=1,weightx=1,insets.left=0"); GridBagUtils.addToPanel(dataSourceOptionsPanel, computeInBetweenPoints, dataSourceOptionsConstraints, "gridwidth=2,gridy=1,gridx=0,weightx=2"); GridBagUtils.addToPanel(dataSourceOptionsPanel, useCorrelativeData, dataSourceOptionsConstraints, "gridy=2,insets.top=16"); GridBagUtils.addToPanel(dataSourceOptionsPanel, correlativeFieldSelector.pointDataSourceLabel, dataSourceOptionsConstraints, "gridy=3,insets.top=0,insets.left=4"); GridBagUtils.addToPanel(dataSourceOptionsPanel, correlativeFieldSelector.pointDataSourceList, dataSourceOptionsConstraints, "gridy=4,insets.left=4"); GridBagUtils.addToPanel(dataSourceOptionsPanel, correlativeFieldSelector.dataFieldLabel, dataSourceOptionsConstraints, "gridy=5,insets.left=4"); GridBagUtils.addToPanel(dataSourceOptionsPanel, correlativeFieldSelector.dataFieldList, dataSourceOptionsConstraints, "gridy=6,insets.left=4"); xAxisRangeControl.getBindingContext().bind(PROPERTY_NAME_MARK_SEGMENTS, new JCheckBox("Mark segments")); yAxisRangeControl.getBindingContext().bind(PROPERTY_NAME_LOG_SCALED, new JCheckBox("Log10 scaled")); JPanel displayOptionsPanel = GridBagUtils.createPanel(); GridBagConstraints displayOptionsConstraints = GridBagUtils.createConstraints("anchor=SOUTH,fill=HORIZONTAL,weightx=1"); GridBagUtils.addToPanel(displayOptionsPanel, xAxisRangeControl.getPanel(), displayOptionsConstraints, "gridy=0"); GridBagUtils.addToPanel(displayOptionsPanel, xAxisRangeControl.getBindingContext().getBinding(PROPERTY_NAME_MARK_SEGMENTS).getComponents()[0], displayOptionsConstraints, "gridy=1"); GridBagUtils.addToPanel(displayOptionsPanel, yAxisRangeControl.getPanel(), displayOptionsConstraints, "gridy=2"); GridBagUtils.addToPanel(displayOptionsPanel, yAxisRangeControl.getBindingContext().getBinding(PROPERTY_NAME_LOG_SCALED).getComponents()[0], displayOptionsConstraints, "gridy=3"); JPanel middlePanel = GridBagUtils.createPanel(); GridBagConstraints middlePanelConstraints = GridBagUtils.createConstraints("anchor=NORTHWEST,fill=HORIZONTAL,insets.top=2,weightx=1"); GridBagUtils.addToPanel(middlePanel, dataSourceOptionsPanel, middlePanelConstraints, "gridy=0"); GridBagUtils.addToPanel(middlePanel, new JPanel(), middlePanelConstraints, "gridy=1,fill=VERTICAL,weighty=1"); GridBagUtils.addToPanel(middlePanel, displayOptionsPanel, middlePanelConstraints, "gridy=2,fill=HORIZONTAL,weighty=0"); return middlePanel; } @Override protected void updateChartData() { //Left empty for Profile Plot Panel } private void configureRendererForCorrelativeData(XYLineAndShapeRenderer renderer) { renderer.setSeriesLinesVisible(1, false); renderer.setSeriesShapesVisible(1, true); renderer.setSeriesStroke(1, new BasicStroke(1.0f)); renderer.setSeriesPaint(1, StatisticChartStyling.CORRELATIVE_POINT_PAINT); renderer.setSeriesFillPaint(1, StatisticChartStyling.CORRELATIVE_POINT_FILL_PAINT); renderer.setSeriesShape(1, StatisticChartStyling.CORRELATIVE_POINT_SHAPE); } @Override protected boolean mustHandleSelectionChange() { return super.mustHandleSelectionChange() || isVectorDataNodeChanged(); } @Override protected void updateComponents() { if (!isInitialized || !isVisible()) { return; } final RasterDataNode raster = getRaster(); if (raster != null) { chart.setTitle(CHART_TITLE + " for " + raster.getName()); } else { chart.setTitle(CHART_TITLE); } correlativeFieldSelector.updatePointDataSource(getProduct()); updateDataSource(); updateDataSet(); updateUIState(); super.updateComponents(); } private void updateDataSource() { if (!isInitialized) { return; } profileData = null; if (getRaster() != null) { try { if (dataSourceConfig.useCorrelativeData && dataSourceConfig.pointDataSource != null) { profileData = new TransectProfileDataBuilder() .raster(getRaster()) .pointData(dataSourceConfig.pointDataSource) .boxSize(dataSourceConfig.boxSize) .connectVertices(dataSourceConfig.computeInBetweenPoints) .useRoiMask(dataSourceConfig.useRoiMask) .roiMask(dataSourceConfig.roiMask) .build(); } else { Shape shape = StatisticsUtils.TransectProfile.getTransectShape(getRaster().getProduct()); if (shape != null) { profileData = new TransectProfileDataBuilder() .raster(getRaster()) .path(shape) .boxSize(dataSourceConfig.boxSize) .connectVertices(dataSourceConfig.computeInBetweenPoints) .useRoiMask(dataSourceConfig.useRoiMask) .roiMask(dataSourceConfig.roiMask) .build(); } } } catch (IOException e) { AbstractDialog.showErrorDialog(getParent(), "Failed to compute profile plot.\n" + "An I/O error occurred:" + e.getMessage(), "I/O error"); } } } private void updateDataSet() { if (!isInitialized) { return; } dataset.removeAllSeries(); double dx = 0.5 * dataSourceConfig.boxSize; if (profileData != null) { final float[] sampleValues = profileData.getSampleValues(); final float[] sampleSigmas = profileData.getSampleSigmas(); XYIntervalSeries series = new XYIntervalSeries(getRaster() != null ? getRaster().getName() : DEFAULT_SAMPLE_DATASET_NAME); for (int x = 0; x < sampleValues.length; x++) { final float y = sampleValues[x]; final float dy = sampleSigmas[x]; series.add(x, x - dx, x + dx, y, y - dy, y + dy); } dataset.addSeries(series); if (dataSourceConfig.useCorrelativeData && dataSourceConfig.pointDataSource != null && dataSourceConfig.dataField != null) { XYIntervalSeries corrSeries = new XYIntervalSeries( StatisticChartStyling.getCorrelativeDataLabel(dataSourceConfig.pointDataSource, dataSourceConfig.dataField)); int[] shapeVertexIndexes = profileData.getShapeVertexIndexes(); SimpleFeature[] simpleFeatures = dataSourceConfig.pointDataSource.getFeatureCollection().toArray(new SimpleFeature[0]); if (shapeVertexIndexes.length == simpleFeatures.length) { int fieldIndex = getAttributeIndex(dataSourceConfig.pointDataSource, dataSourceConfig.dataField); if (fieldIndex != -1) { for (int i = 0; i < simpleFeatures.length; i++) { Number attribute = (Number) simpleFeatures[i].getAttribute(fieldIndex); if (attribute != null) { final double x = shapeVertexIndexes[i]; final double y = attribute.doubleValue(); corrSeries.add(x, x, x, y, y, y); } } dataset.addSeries(corrSeries); } } else { System.out.println("Weird things happened:"); System.out.println(" shapeVertexIndexes.length = " + shapeVertexIndexes.length); System.out.println(" simpleFeatures.length = " + simpleFeatures.length); } } profilePlotDisplay.restoreAutoBounds(); xAxisRangeControl.getBindingContext().setComponentsEnabled(PROPERTY_NAME_MARK_SEGMENTS, profileData.getShapeVertices().length > 2); } } private int getAttributeIndex(VectorDataNode pointDataSource, AttributeDescriptor dataField) { final String fieldName = dataField.getLocalName(); if (fieldName.equals(CorrelativeFieldSelector.NULL_NAME)) { return -1; } return pointDataSource.getFeatureType().indexOf(fieldName); } private void updateUIState() { if (!isInitialized) { return; } xAxisRangeControl.getBindingContext().setComponentsEnabled(PROPERTY_NAME_MARK_SEGMENTS, profileData != null && profileData.getShapeVertices().length > 2); xAxisRangeControl.setComponentsEnabled(profileData != null); yAxisRangeControl.setComponentsEnabled(profileData != null); adjustPlotAxes(); if (dataSourceConfig.computeInBetweenPoints) { chart.getXYPlot().setRenderer(deviationRenderer); } else { chart.getXYPlot().setRenderer(pointRenderer); } chart.getXYPlot().getRangeAxis().setLabel(StatisticChartStyling.getAxisLabel(getRaster(), DEFAULT_SAMPLE_DATASET_NAME, false)); boolean markSegments = xAxisRangeControl.getBindingContext().getPropertySet().getValue(PROPERTY_NAME_MARK_SEGMENTS); if (markSegments && profileData != null && profileData.getNumShapeVertices() > 1) { final int[] shapeVertexIndexes = profileData.getShapeVertexIndexes(); removeIntervalMarkers(); for (int i = 0; i < shapeVertexIndexes.length - 1; i++) { if (i % 2 != 0) { final IntervalMarker marker = new IntervalMarker(shapeVertexIndexes[i], shapeVertexIndexes[i + 1]); marker.setPaint(new Color(120, 122, 125)); marker.setAlpha(0.3F); chart.getXYPlot().addDomainMarker(marker, Layer.BACKGROUND); intervalMarkers.add(marker); } } } else { removeIntervalMarkers(); } pointDataSourceEnablement.apply(); dataFieldEnablement.apply(); } private void removeIntervalMarkers() { for (IntervalMarker intervalMarker : intervalMarkers) { chart.getXYPlot().removeDomainMarker(intervalMarker, Layer.BACKGROUND); } intervalMarkers.clear(); } private void adjustAxisControlComponents() { if (!axisAdjusting) { axisAdjusting = true; try { if (xAxisRangeControl.isAutoMinMax()) { xAxisRangeControl.adjustComponents(chart.getXYPlot().getDomainAxis(), 0); } if (yAxisRangeControl.isAutoMinMax()) { yAxisRangeControl.adjustComponents(chart.getXYPlot().getRangeAxis(), 2); } } finally { axisAdjusting = false; } } } private void adjustPlotAxes() { if (!axisAdjusting) { axisAdjusting = true; try { xAxisRangeControl.adjustAxis(chart.getXYPlot().getDomainAxis(), 0); yAxisRangeControl.adjustAxis(chart.getXYPlot().getRangeAxis(), 2); } finally { axisAdjusting = false; } } } private void updateScalingOfYAxis() { final boolean logScaled = (Boolean) yAxisRangeControl.getBindingContext().getBinding(PROPERTY_NAME_LOG_SCALED).getPropertyValue(); final XYPlot plot = chart.getXYPlot(); plot.setRangeAxis(StatisticChartStyling.updateScalingOfAxis(logScaled, plot.getRangeAxis(), true)); } @Override public void nodeAdded(ProductNodeEvent event) { if (event.getSourceNode() instanceof VectorDataNode) { updateComponents(); } } @Override public void nodeRemoved(ProductNodeEvent event) { if (event.getSourceNode() instanceof VectorDataNode) { updateComponents(); } } @Override public void setVisible(boolean aFlag) { super.setVisible(aFlag); updateComponents(); } @Override protected String getDataAsText() { if (profileData != null) { ProfileDataTableModel model = createProfileDataTableModel(); return model.toCsv(); } else { return ""; } } private ProfileDataTableModel createProfileDataTableModel() { return new ProfileDataTableModel(getRaster().getName(), profileData, dataSourceConfig); } @Override public void handleLayerContentChanged() { updateComponents(); } @SuppressWarnings("UnusedDeclaration") static class DataSourceConfig { int boxSize = 3; boolean useRoiMask; Mask roiMask; boolean computeInBetweenPoints = true; boolean useCorrelativeData; VectorDataNode pointDataSource; AttributeDescriptor dataField; } private class EnablePointDataCondition extends Enablement.Condition { @Override public boolean evaluate(BindingContext bindingContext) { return dataSourceConfig.useCorrelativeData && getProduct() != null; } } }