/* * Copyright (C) 2015 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.bandmaths; import com.bc.ceres.binding.PropertyContainer; import com.bc.ceres.binding.PropertyDescriptor; import com.bc.ceres.binding.ValueSet; import com.bc.ceres.core.Assert; import com.bc.ceres.swing.binding.BindingContext; import com.bc.ceres.swing.binding.PropertyEditor; import com.bc.ceres.swing.binding.PropertyEditorRegistry; import com.bc.ceres.swing.binding.internal.CheckBoxEditor; import com.bc.ceres.swing.binding.internal.NumericEditor; import com.bc.ceres.swing.binding.internal.SingleSelectionEditor; import com.bc.ceres.swing.binding.internal.TextComponentAdapter; import com.bc.ceres.swing.binding.internal.TextFieldEditor; import org.esa.snap.core.datamodel.Band; import org.esa.snap.core.datamodel.Product; import org.esa.snap.core.datamodel.ProductData; import org.esa.snap.core.datamodel.ProductNodeGroup; import org.esa.snap.core.datamodel.ProductNodeList; import org.esa.snap.core.datamodel.RasterDataNode; import org.esa.snap.core.datamodel.VirtualBand; import org.esa.snap.core.dataop.barithm.BandArithmetic; import org.esa.snap.core.dataop.barithm.RasterDataSymbol; import org.esa.snap.core.jexp.ParseException; import org.esa.snap.core.jexp.Term; import org.esa.snap.core.util.ProductUtils; import org.esa.snap.rcp.SnapApp; import org.esa.snap.rcp.actions.window.OpenImageViewAction; import org.esa.snap.rcp.nodes.UndoableProductNodeInsertion; import org.esa.snap.rcp.util.Dialogs; import org.esa.snap.ui.GridBagUtils; import org.esa.snap.ui.ModalDialog; import org.esa.snap.ui.product.ProductExpressionPane; import org.openide.awt.UndoRedo; import org.openide.util.NbBundle; import javax.swing.JButton; import javax.swing.JComponent; import javax.swing.JLabel; import javax.swing.JPanel; import javax.swing.JTextArea; import java.awt.BorderLayout; import java.awt.GridBagConstraints; import java.awt.event.ActionListener; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.PrintStream; import java.nio.file.Files; import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; import java.util.List; import java.util.Set; import static org.esa.snap.rcp.SnapApp.SelectionSourceHint.EXPLORER; @NbBundle.Messages({ "CTL_BandMathsDialog_Title=Band Maths", "CTL_BandMathsDialog_ErrBandNotCreated=The band could not be created.\nAn expression parse error occurred:\n", "CTL_BandMathsDialog_ErrExpressionNotValid=Please check the band maths expression you have entered.\nIt is not valid.", "CTL_BandMathsDialog_ErrBandCannotBeReferenced=You cannot reference the target band ''{0}'' within the expression.", "CTL_BandMathsDialog_LblExpression=Band maths expression:", }) class BandMathsDialog extends ModalDialog { static final String PREF_KEY_AUTO_SHOW_NEW_BANDS = "BandMaths.autoShowNewBands"; static final String PREF_KEY_LAST_EXPRESSION_PATH = "BandMaths.lastExpressionPath"; private static final String PROPERTY_NAME_PRODUCT = "productName"; private static final String PROPERTY_NAME_EXPRESSION = "expression"; private static final String PROPERTY_NAME_NO_DATA_VALUE = "noDataValue"; private static final String PROPERTY_NAME_NO_DATA_VALUE_USED = "noDataValueUsed"; private static final String PROPERTY_NAME_SAVE_EXPRESSION_ONLY = "saveExpressionOnly"; private static final String PROPERTY_NAME_GENERATE_UNCERTAINTY_BAND = "generateUncertaintyBand"; private static final String PROPERTY_NAME_BAND_NAME = "bandName"; private static final String PROPERTY_NAME_BAND_DESC = "bandDescription"; private static final String PROPERTY_NAME_BAND_UNIT = "bandUnit"; private static final String PROPERTY_NAME_BAND_WAVELENGTH = "bandWavelength"; private final ProductNodeList<Product> productsList; private final BindingContext bindingContext; private Product targetProduct; private String productName; @SuppressWarnings("FieldCanBeLocal") private String expression; @SuppressWarnings("UnusedDeclaration") private double noDataValue; @SuppressWarnings("UnusedDeclaration") private boolean noDataValueUsed; @SuppressWarnings("UnusedDeclaration") private boolean saveExpressionOnly; @SuppressWarnings("UnusedDeclaration") private boolean generateUncertaintyBand; @SuppressWarnings("UnusedDeclaration") private String bandName; @SuppressWarnings("FieldCanBeLocal") private String bandDescription; @SuppressWarnings("FieldCanBeLocal") private String bandUnit; @SuppressWarnings("UnusedDeclaration") private float bandWavelength; private static int numNewBands = 0; public BandMathsDialog(Product currentProduct, ProductNodeList<Product> productsList, String expression, String helpId) { super(SnapApp.getDefault().getMainFrame(), Bundle.CTL_BandMathsDialog_Title(), ID_OK_CANCEL_HELP, helpId); Assert.notNull(expression, "expression"); Assert.notNull(currentProduct, "currentProduct"); Assert.notNull(productsList, "productsList"); Assert.argument(productsList.size() > 0, "productsList must be not empty"); targetProduct = currentProduct; this.productsList = productsList; bindingContext = createBindingContext(); this.expression = expression; bandDescription = ""; bandUnit = ""; makeUI(); } @Override protected void onOK() { final String validMaskExpression; int width = targetProduct.getSceneRasterWidth(); int height = targetProduct.getSceneRasterHeight(); RasterDataNode prototypeRasterDataNode = null; try { Product[] products = getCompatibleProducts(); int defaultProductIndex = Arrays.asList(products).indexOf(targetProduct); validMaskExpression = BandArithmetic.getValidMaskExpression(getExpression(), products, defaultProductIndex, null); final RasterDataNode[] refRasters = BandArithmetic.getRefRasters(getExpression(), products, defaultProductIndex); if (refRasters.length > 0) { prototypeRasterDataNode = refRasters[0]; width = prototypeRasterDataNode.getRasterWidth(); height = prototypeRasterDataNode.getRasterHeight(); } } catch (ParseException e) { String errorMessage = Bundle.CTL_BandMathsDialog_ErrBandNotCreated() + e.getMessage(); Dialogs.showError(Bundle.CTL_BandMathsDialog_Title() + " - Error", errorMessage); hide(); return; } Band band; if (saveExpressionOnly) { band = new VirtualBand(getBandName(), ProductData.TYPE_FLOAT32, width, height, getExpression()); setBandProperties(band, validMaskExpression); } else { band = new Band(getBandName(), ProductData.TYPE_FLOAT32, width, height); setBandProperties(band, ""); } ProductNodeGroup<Band> bandGroup = targetProduct.getBandGroup(); bandGroup.add(band); if (prototypeRasterDataNode != null) { ProductUtils.copyImageGeometry(prototypeRasterDataNode, band, false); } if (saveExpressionOnly) { checkExpressionForExternalReferences(getExpression()); } else { String expression = getExpression(); if (validMaskExpression != null && !validMaskExpression.isEmpty()) { expression = "(" + validMaskExpression + ") ? (" + expression + ") : NaN"; } band.setSourceImage(VirtualBand.createSourceImage(band, expression)); } UndoRedo.Manager undoManager = SnapApp.getDefault().getUndoManager(targetProduct); if (undoManager != null) { undoManager.addEdit(new UndoableProductNodeInsertion<>(bandGroup, band)); } hide(); band.setModified(true); if (SnapApp.getDefault().getPreferences().getBoolean(PREF_KEY_AUTO_SHOW_NEW_BANDS, true)) { OpenImageViewAction.openImageView(band); } if (generateUncertaintyBand) { if (band instanceof VirtualBand) { VirtualBand virtualBand = (VirtualBand) band; PropagateUncertaintyAction uncertaintyAction = new PropagateUncertaintyAction(virtualBand); uncertaintyAction.actionPerformed(null); } } } private void setBandProperties(Band band, String validMaskExpression) { band.setDescription(bandDescription); band.setUnit(bandUnit); band.setSpectralWavelength(bandWavelength); band.setGeophysicalNoDataValue(noDataValue); band.setNoDataValueUsed(noDataValueUsed); band.setValidPixelExpression(validMaskExpression); } @Override protected boolean verifyUserInput() { if (!isValidExpression()) { showErrorDialog(Bundle.CTL_BandMathsDialog_ErrExpressionNotValid()); return false; } if (isTargetBandReferencedInExpression()) { showErrorDialog(Bundle.CTL_BandMathsDialog_ErrBandCannotBeReferenced(getBandName())); return false; } return super.verifyUserInput(); } private void makeUI() { JButton loadExpressionButton = new JButton("Load..."); loadExpressionButton.setName("loadExpressionButton"); loadExpressionButton.addActionListener(createLoadExpressionButtonListener()); JButton saveExpressionButton = new JButton("Save..."); saveExpressionButton.setName("saveExpressionButton"); saveExpressionButton.addActionListener(createSaveExpressionButtonListener()); JButton editExpressionButton = new JButton("Edit Expression..."); editExpressionButton.setName("editExpressionButton"); editExpressionButton.addActionListener(createEditExpressionButtonListener()); final JPanel panel = GridBagUtils.createPanel(); int line = 0; GridBagConstraints gbc = new GridBagConstraints(); JComponent[] components = createComponents(PROPERTY_NAME_PRODUCT, SingleSelectionEditor.class); gbc.gridy = ++line; GridBagUtils.addToPanel(panel, components[1], gbc, "gridwidth=3, fill=BOTH, weightx=1"); gbc.gridy = ++line; GridBagUtils.addToPanel(panel, components[0], gbc, "insets.top=3, gridwidth=3, fill=BOTH, anchor=WEST"); gbc.gridy = ++line; components = createComponents(PROPERTY_NAME_BAND_NAME, TextFieldEditor.class); GridBagUtils.addToPanel(panel, components[1], gbc, "weightx=0, insets.top=3, gridwidth=1, fill=HORIZONTAL, anchor=WEST"); GridBagUtils.addToPanel(panel, components[0], gbc, "weightx=1, insets.top=3, gridwidth=2, fill=HORIZONTAL, anchor=WEST"); gbc.gridy = ++line; components = createComponents(PROPERTY_NAME_BAND_DESC, TextFieldEditor.class); GridBagUtils.addToPanel(panel, components[1], gbc, "weightx=0, insets.top=3, gridwidth=1, fill=HORIZONTAL, anchor=WEST"); GridBagUtils.addToPanel(panel, components[0], gbc, "weightx=1, insets.top=3, gridwidth=2, fill=HORIZONTAL, anchor=WEST"); gbc.gridy = ++line; components = createComponents(PROPERTY_NAME_BAND_UNIT, TextFieldEditor.class); GridBagUtils.addToPanel(panel, components[1], gbc, "weightx=0, insets.top=3, gridwidth=1, fill=HORIZONTAL, anchor=WEST"); GridBagUtils.addToPanel(panel, components[0], gbc, "weightx=1, insets.top=3, gridwidth=2, fill=HORIZONTAL, anchor=WEST"); gbc.gridy = ++line; components = createComponents(PROPERTY_NAME_BAND_WAVELENGTH, TextFieldEditor.class); GridBagUtils.addToPanel(panel, components[1], gbc, "weightx=0, insets.top=3, gridwidth=1, fill=HORIZONTAL, anchor=WEST"); GridBagUtils.addToPanel(panel, components[0], gbc, "weightx=1, insets.top=3, gridwidth=2, fill=HORIZONTAL, anchor=WEST"); gbc.gridy = ++line; components = createComponents(PROPERTY_NAME_SAVE_EXPRESSION_ONLY, CheckBoxEditor.class); GridBagUtils.addToPanel(panel, components[0], gbc, "insets.top=3, gridwidth=3, fill=HORIZONTAL, anchor=EAST"); gbc.gridy = ++line; JPanel nodataPanel = new JPanel(new BorderLayout()); components = createComponents(PROPERTY_NAME_NO_DATA_VALUE_USED, CheckBoxEditor.class); nodataPanel.add(components[0], BorderLayout.WEST); components = createComponents(PROPERTY_NAME_NO_DATA_VALUE, NumericEditor.class); nodataPanel.add(components[0]); GridBagUtils.addToPanel(panel, nodataPanel, gbc, "weightx=1, insets.top=3, gridwidth=3, fill=HORIZONTAL, anchor=WEST"); gbc.gridy = ++line; components = createComponents(PROPERTY_NAME_GENERATE_UNCERTAINTY_BAND, CheckBoxEditor.class); GridBagUtils.addToPanel(panel, components[0], gbc, "insets.top=3, gridwidth=3, fill=HORIZONTAL, anchor=EAST"); gbc.gridy = ++line; JLabel expressionLabel = new JLabel(Bundle.CTL_BandMathsDialog_LblExpression()); JTextArea expressionArea = new JTextArea(); expressionArea.setRows(3); TextComponentAdapter textComponentAdapter = new TextComponentAdapter(expressionArea); bindingContext.bind(PROPERTY_NAME_EXPRESSION, textComponentAdapter); GridBagUtils.addToPanel(panel, expressionLabel, gbc, "insets.top=3, gridwidth=3, anchor=WEST"); gbc.gridy = ++line; GridBagUtils.addToPanel(panel, expressionArea, gbc, "weighty=1, insets.top=3, gridwidth=3, fill=BOTH, anchor=WEST"); gbc.gridy = ++line; final JPanel loadSavePanel = new JPanel(); loadSavePanel.add(loadExpressionButton); loadSavePanel.add(saveExpressionButton); GridBagUtils.addToPanel(panel, loadSavePanel, gbc, "weighty=0, insets.top=3, gridwidth=2, fill=NONE, anchor=WEST"); GridBagUtils.addToPanel(panel, editExpressionButton, gbc, "weighty=1, insets.top=3, gridwidth=1, fill=HORIZONTAL, anchor=EAST"); gbc.gridy = ++line; GridBagUtils.addToPanel(panel, new JLabel(""), gbc, "insets.top=10, weightx=1, weighty=1, gridwidth=3, fill=BOTH, anchor=WEST"); setContent(panel); expressionArea.selectAll(); expressionArea.requestFocus(); } private ActionListener createLoadExpressionButtonListener() { return e -> { try { final File file = Dialogs.requestFileForOpen( "Load Band Maths Expression", false, null, PREF_KEY_LAST_EXPRESSION_PATH); if (file != null) { expression = new String(Files.readAllBytes(file.toPath())); bindingContext.getBinding(PROPERTY_NAME_EXPRESSION).setPropertyValue(expression); bindingContext.getBinding(PROPERTY_NAME_EXPRESSION).adjustComponents(); } } catch (IOException ex) { showErrorDialog(ex.getMessage()); } }; } private ActionListener createSaveExpressionButtonListener() { return e -> { try { final File file = Dialogs.requestFileForSave( "Save Band Maths Expression", false, null, ".txt", "myExpression", null, PREF_KEY_LAST_EXPRESSION_PATH); if (file != null) { final FileOutputStream out = new FileOutputStream(file.getAbsolutePath(), false); PrintStream p = new PrintStream(out); p.print(getExpression()); } } catch (IOException ex) { showErrorDialog(ex.getMessage()); } }; } private JComponent[] createComponents(String propertyName, Class<? extends PropertyEditor> editorClass) { PropertyDescriptor descriptor = bindingContext.getPropertySet().getDescriptor(propertyName); PropertyEditor editor = PropertyEditorRegistry.getInstance().getPropertyEditor(editorClass.getName()); return editor.createComponents(descriptor, bindingContext); } private BindingContext createBindingContext() { final PropertyContainer container = PropertyContainer.createObjectBacked(this); final BindingContext context = new BindingContext(container); container.addPropertyChangeListener(PROPERTY_NAME_PRODUCT, evt -> targetProduct = productsList.getByDisplayName(productName)); productName = targetProduct.getDisplayName(); PropertyDescriptor descriptor = container.getDescriptor(PROPERTY_NAME_PRODUCT); descriptor.setValueSet(new ValueSet(productsList.getDisplayNames())); descriptor.setDisplayName("Target product"); descriptor = container.getDescriptor(PROPERTY_NAME_BAND_NAME); descriptor.setDisplayName("Name"); descriptor.setDescription("The name for the new band."); descriptor.setNotEmpty(true); descriptor.setValidator(new ProductNodeNameValidator(targetProduct)); String newBandName; do { numNewBands++; newBandName = "new_band_" + numNewBands; } while (targetProduct.containsRasterDataNode(newBandName)); descriptor.setDefaultValue("new_band_" + (numNewBands)); descriptor = container.getDescriptor(PROPERTY_NAME_BAND_DESC); descriptor.setDisplayName("Description"); descriptor.setDescription("The description for the new band."); descriptor = container.getDescriptor(PROPERTY_NAME_BAND_UNIT); descriptor.setDisplayName("Unit"); descriptor.setDescription("The physical unit for the new band."); descriptor = container.getDescriptor(PROPERTY_NAME_BAND_WAVELENGTH); descriptor.setDisplayName("Spectral wavelength"); descriptor.setDescription("The physical unit for the new band."); descriptor = container.getDescriptor(PROPERTY_NAME_EXPRESSION); descriptor.setDisplayName("Band maths expression"); descriptor.setDescription("Band maths expression"); descriptor.setNotEmpty(true); descriptor = container.getDescriptor(PROPERTY_NAME_SAVE_EXPRESSION_ONLY); descriptor.setDisplayName("Virtual (save expression only, don't store data)"); descriptor.setDefaultValue(Boolean.TRUE); descriptor = container.getDescriptor(PROPERTY_NAME_NO_DATA_VALUE_USED); descriptor.setDisplayName("Replace NaN and infinity results by"); descriptor.setDefaultValue(Boolean.TRUE); descriptor = container.getDescriptor(PROPERTY_NAME_NO_DATA_VALUE); descriptor.setDefaultValue(Double.NaN); descriptor = container.getDescriptor(PROPERTY_NAME_GENERATE_UNCERTAINTY_BAND); descriptor.setDisplayName("Generate associated uncertainty band"); descriptor.setDefaultValue(Boolean.FALSE); container.setDefaultValues(); context.addPropertyChangeListener(PROPERTY_NAME_SAVE_EXPRESSION_ONLY, evt -> { final boolean saveExpressionOnly1 = (Boolean) context.getBinding( PROPERTY_NAME_SAVE_EXPRESSION_ONLY).getPropertyValue(); if (!saveExpressionOnly1) { context.getBinding(PROPERTY_NAME_NO_DATA_VALUE_USED).setPropertyValue(true); } }); context.bindEnabledState(PROPERTY_NAME_NO_DATA_VALUE_USED, false, PROPERTY_NAME_SAVE_EXPRESSION_ONLY, Boolean.FALSE); context.bindEnabledState(PROPERTY_NAME_NO_DATA_VALUE, true, PROPERTY_NAME_NO_DATA_VALUE_USED, Boolean.TRUE); context.bindEnabledState(PROPERTY_NAME_GENERATE_UNCERTAINTY_BAND, true, PROPERTY_NAME_SAVE_EXPRESSION_ONLY, Boolean.TRUE); return context; } private String getBandName() { return bandName.trim(); } private String getExpression() { return expression.trim(); } private Product[] getCompatibleProducts() { List<Product> compatibleProducts = new ArrayList<>(productsList.size()); compatibleProducts.add(targetProduct); for (int i = 0; i < productsList.size(); i++) { final Product product = productsList.getAt(i); if (targetProduct != product) { if (targetProduct.getSceneRasterWidth() == product.getSceneRasterWidth() && targetProduct.getSceneRasterHeight() == product.getSceneRasterHeight()) { compatibleProducts.add(product); } } } return compatibleProducts.toArray(new Product[compatibleProducts.size()]); } private ActionListener createEditExpressionButtonListener() { return e -> { Product[] compatibleProducts = getCompatibleProducts(); ProductExpressionPane pep = ProductExpressionPane.createGeneralExpressionPane(compatibleProducts, targetProduct, SnapApp.getDefault().getPreferencesPropertyMap()); pep.setCode(getExpression()); int status = pep.showModalDialog(getJDialog(), "Band Maths Expression Editor"); if (status == ModalDialog.ID_OK) { bindingContext.getBinding(PROPERTY_NAME_EXPRESSION).setPropertyValue(pep.getCode()); } pep.dispose(); }; } private void checkExpressionForExternalReferences(String expression) { final Product[] compatibleProducts = getCompatibleProducts(); if (compatibleProducts.length > 1) { int defaultIndex = Arrays.asList(compatibleProducts).indexOf(targetProduct); RasterDataNode[] rasters = null; try { rasters = BandArithmetic.getRefRasters(expression, compatibleProducts, defaultIndex); } catch (ParseException ignored) { } if (rasters != null && rasters.length > 0) { Set<Product> externalProducts = new HashSet<>(compatibleProducts.length); for (RasterDataNode rdn : rasters) { Product product = rdn.getProduct(); if (product != targetProduct) { externalProducts.add(product); } } if (!externalProducts.isEmpty()) { String message = "The entered maths expression references multiple products.\n" + "It will cause problems unless the session is restored as is.\n\n" + "Note: You can save the session from the file menu."; Dialogs.showWarning(message); } } } } private boolean isValidExpression() { final Product[] products = getCompatibleProducts(); if (products.length == 0 || getExpression().isEmpty()) { return false; } final int defaultIndex = Arrays.asList(products).indexOf(targetProduct); try { BandArithmetic.parseExpression(getExpression(), products, defaultIndex == -1 ? 0 : defaultIndex); return true; } catch (ParseException e) { return false; } } private boolean isTargetBandReferencedInExpression() { final Product[] products = getCompatibleProducts(); final int defaultIndex = Arrays.asList(products).indexOf(SnapApp.getDefault().getSelectedProduct(EXPLORER)); try { final Term term = BandArithmetic.parseExpression(getExpression(), products, defaultIndex == -1 ? 0 : defaultIndex); final RasterDataSymbol[] refRasterDataSymbols = BandArithmetic.getRefRasterDataSymbols(term); String bName = getBandName(); if (targetProduct.containsRasterDataNode(bName)) { for (final RasterDataSymbol refRasterDataSymbol : refRasterDataSymbols) { final String refRasterName = refRasterDataSymbol.getRaster().getName(); if (bName.equalsIgnoreCase(refRasterName)) { return true; } } } } catch (ParseException e) { return false; } return false; } }