/*
* Copyright (C) 2014 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.actions.tools;
import com.bc.ceres.core.Assert;
import com.bc.ceres.core.ProgressMonitor;
import com.bc.ceres.swing.progress.ProgressMonitorSwingWorker;
import org.esa.snap.core.datamodel.Band;
import org.esa.snap.core.datamodel.BasicPixelGeoCoding;
import org.esa.snap.core.datamodel.GeoCodingFactory;
import org.esa.snap.core.datamodel.PixelGeoCoding;
import org.esa.snap.core.datamodel.Product;
import org.esa.snap.core.datamodel.ProductNode;
import org.esa.snap.core.util.ArrayUtils;
import org.esa.snap.core.util.StringUtils;
import org.esa.snap.rcp.SnapApp;
import org.esa.snap.rcp.util.Dialogs;
import org.esa.snap.ui.ExpressionPane;
import org.esa.snap.ui.GridBagUtils;
import org.esa.snap.ui.ModalDialog;
import org.esa.snap.ui.UIUtils;
import org.esa.snap.ui.product.ProductExpressionPane;
import org.openide.awt.ActionID;
import org.openide.awt.ActionReference;
import org.openide.awt.ActionRegistration;
import org.openide.awt.UndoRedo;
import org.openide.util.ContextAwareAction;
import org.openide.util.Lookup;
import org.openide.util.LookupEvent;
import org.openide.util.LookupListener;
import org.openide.util.NbBundle.Messages;
import org.openide.util.Utilities;
import org.openide.util.WeakListeners;
import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.JButton;
import javax.swing.JComboBox;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JSpinner;
import javax.swing.JTextField;
import javax.swing.SwingUtilities;
import javax.swing.undo.AbstractUndoableEdit;
import javax.swing.undo.CannotRedoException;
import javax.swing.undo.CannotUndoException;
import java.awt.Dimension;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.awt.Window;
import java.awt.event.ActionEvent;
import java.io.IOException;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.ExecutionException;
import java.util.logging.Level;
@ActionID(
category = "Tools",
id = "AttachPixelGeoCodingAction"
)
@ActionRegistration(
displayName = "#CTL_AttachPixelGeoCodingActionText",
popupText = "#CTL_AttachPixelGeoCodingActionText",
lazy = false
)
@ActionReference(path = "Menu/Tools", position = 210, separatorBefore = 200)
@Messages({
"CTL_AttachPixelGeoCodingActionText=Attach Pixel Geo-Coding...",
"CTL_AttachPixelGeoCodingDialogTitle=Attach Pixel Geo-Coding",
"CTL_AttachPixelGeoCodingDialogDescription=Attach a pixel based geo-coding to the selected product"
})
public class AttachPixelGeoCodingAction extends AbstractAction implements ContextAwareAction, LookupListener {
private static final String HELP_ID = "pixelGeoCodingSetup";
private final Lookup lkp;
public AttachPixelGeoCodingAction() {
this(Utilities.actionsGlobalContext());
}
public AttachPixelGeoCodingAction(Lookup lkp) {
super(Bundle.CTL_AttachPixelGeoCodingActionText());
this.lkp = lkp;
Lookup.Result<ProductNode> lkpContext = lkp.lookupResult(ProductNode.class);
lkpContext.addLookupListener(WeakListeners.create(LookupListener.class, this, lkpContext));
putValue(Action.SHORT_DESCRIPTION, Bundle.CTL_AttachPixelGeoCodingDialogDescription());
setEnableState();
}
@Override
public Action createContextAwareInstance(Lookup actionContext) {
return new AttachPixelGeoCodingAction(actionContext);
}
@Override
public void resultChanged(LookupEvent ev) {
setEnableState();
}
@Override
public void actionPerformed(ActionEvent actionEvent) {
Product selectedProduct = lkp.lookup(ProductNode.class).getProduct();
final Band[] bands = selectedProduct.getBands();
int validBandsCount = 0;
for (Band band : bands) {
if (band.getRasterSize().equals(selectedProduct.getSceneRasterSize())) {
validBandsCount++;
if (validBandsCount == 2) {
break;
}
}
}
if (validBandsCount < 2) {
Dialogs.showError("Pixel Geo-Coding cannot be attached: Too few bands of product scene size");
return;
}
attachPixelGeoCoding(selectedProduct);
}
private void setEnableState() {
ProductNode productNode = lkp.lookup(ProductNode.class);
boolean state = false;
if (productNode != null) {
Product product = productNode.getProduct();
if (product != null) {
final boolean hasPixelGeoCoding = product.getSceneGeoCoding() instanceof BasicPixelGeoCoding;
final boolean hasSomeBands = product.getNumBands() >= 2;
state = !hasPixelGeoCoding && hasSomeBands;
}
}
setEnabled(state);
}
private static void attachPixelGeoCoding(final Product product) {
final SnapApp snapApp = SnapApp.getDefault();
final Window mainFrame = snapApp.getMainFrame();
String dialogTitle = Bundle.CTL_AttachPixelGeoCodingDialogTitle();
final PixelGeoCodingSetupDialog setupDialog = new PixelGeoCodingSetupDialog(mainFrame,
dialogTitle,
HELP_ID,
product);
if (setupDialog.show() != ModalDialog.ID_OK) {
return;
}
final Band lonBand = setupDialog.getSelectedLonBand();
final Band latBand = setupDialog.getSelectedLatBand();
final int searchRadius = setupDialog.getSearchRadius();
final String validMask = setupDialog.getValidMask();
final String msgPattern = "New Pixel Geo-Coding: lon = ''{0}'' ; lat = ''{1}'' ; radius=''{2}'' ; mask=''{3}''";
snapApp.getLogger().log(Level.INFO, MessageFormat.format(msgPattern,
lonBand.getName(), latBand.getName(),
searchRadius, validMask));
final long requiredBytes = PixelGeoCoding.getRequiredMemory(product, validMask != null);
final long requiredMegas = requiredBytes / (1024 * 1024);
final long freeMegas = Runtime.getRuntime().freeMemory() / (1024 * 1024);
if (freeMegas < requiredMegas) {
final String message = MessageFormat.format("This operation requires to load at least {0} M\n" +
"of additional data into memory.\n\n" +
"Do you really want to continue?",
requiredMegas);
final Dialogs.Answer answer = Dialogs.requestDecision(dialogTitle, message, false, "load_latlon_band_data");
if (answer != Dialogs.Answer.YES) {
return;
}
}
UIUtils.setRootFrameWaitCursor(mainFrame);
final ProgressMonitorSwingWorker<Void, Void> swingWorker = new ProgressMonitorSwingWorker<Void, Void>(mainFrame, dialogTitle) {
@Override
protected Void doInBackground(ProgressMonitor pm) throws Exception {
final BasicPixelGeoCoding pixelGeoCoding = GeoCodingFactory.createPixelGeoCoding(latBand, lonBand, validMask, searchRadius, pm);
product.setSceneGeoCoding(pixelGeoCoding);
UndoRedo.Manager undoManager = SnapApp.getDefault().getUndoManager(product);
if (undoManager != null) {
undoManager.addEdit(new UndoableAttachGeoCoding<>(product, pixelGeoCoding));
}
return null;
}
@Override
public void done() {
try {
get();
Dialogs.showInformation(dialogTitle, "Pixel geo-coding has been attached.", null);
} catch (Exception e) {
Throwable cause = e;
if (e instanceof ExecutionException) {
cause = e.getCause();
}
String msg = "An internal error occurred:\n" + e.getMessage();
if (cause instanceof IOException) {
msg = "An I/O error occurred:\n" + e.getMessage();
}
Dialogs.showError(dialogTitle, msg);
} finally {
UIUtils.setRootFrameDefaultCursor(mainFrame);
}
}
};
swingWorker.executeWithBlocking();
}
private static class PixelGeoCodingSetupDialog extends ModalDialog {
private Product product;
private String selectedLonBand;
private String selectedLatBand;
private String[] bandNames;
private JComboBox<String> lonBox;
private JComboBox<String> latBox;
private JTextField validMaskField;
private JSpinner radiusSpinner;
private final int defaultRadius = 6;
private final int minRadius = 0;
private final int maxRadius = 99;
private final int bigRadiusStep = 0;
private final int smallRadiusStep = 1;
public PixelGeoCodingSetupDialog(final Window parent, final String title,
final String helpID, final Product product) {
super(parent, title, ModalDialog.ID_OK_CANCEL_HELP, helpID);
this.product = product;
final Band[] bands = product.getBands();
if (product.isMultiSize()) {
List<String> bandNameList = new ArrayList<>();
for (Band band : bands) {
if (band.getRasterSize().equals(product.getSceneRasterSize())) {
bandNameList.add(band.getName());
}
}
bandNames = bandNameList.toArray(new String[bandNameList.size()]);
} else {
bandNames = Arrays.stream(bands).map(Band::getName).toArray(String[]::new);
}
}
@Override
public int show() {
createUI();
return super.show();
}
public Band getSelectedLonBand() {
return product.getBand(selectedLonBand);
}
public Band getSelectedLatBand() {
return product.getBand(selectedLatBand);
}
public int getSearchRadius() {
return ((Number) radiusSpinner.getValue()).intValue();
}
public String getValidMask() {
return validMaskField.getText();
}
@Override
protected void onOK() {
final String lonValue = (String) lonBox.getSelectedItem();
selectedLonBand = findBandName(lonValue);
final String latValue = (String) latBox.getSelectedItem();
selectedLatBand = findBandName(latValue);
if (selectedLatBand == null || selectedLonBand == null || Objects.equals(selectedLatBand, selectedLonBand)) {
Dialogs.showWarning(Bundle.CTL_AttachPixelGeoCodingDialogTitle(),
"You have to select two different bands for the pixel geo-coding.",
null);
} else {
super.onOK();
}
}
@Override
protected void onCancel() {
selectedLatBand = null;
selectedLonBand = null;
super.onCancel();
}
private void createUI() {
final JPanel panel = new JPanel(new GridBagLayout());
final GridBagConstraints gbc = GridBagUtils.createDefaultConstraints();
final JLabel lonLabel = new JLabel("Longitude band:");
final JLabel latLabel = new JLabel("Latitude band:");
final JLabel radiusLabel = new JLabel("Search radius:");
final JLabel maskLabel = new JLabel("Valid mask:");
lonBox = new JComboBox<>(bandNames);
latBox = new JComboBox<>(bandNames);
doPreSelection(lonBox, "lon");
doPreSelection(latBox, "lat");
radiusSpinner = UIUtils.createSpinner(defaultRadius, minRadius, maxRadius,
smallRadiusStep, bigRadiusStep, "#0");
validMaskField = new JTextField(createDefaultValidMask(product));
validMaskField.setCaretPosition(0);
final JButton exprDialogButton = new JButton("...");
exprDialogButton.addActionListener(e -> {
invokeExpressionEditor();
});
final int preferredSize = validMaskField.getPreferredSize().height;
exprDialogButton.setPreferredSize(new Dimension(preferredSize, preferredSize));
radiusSpinner.setPreferredSize(new Dimension(60, preferredSize));
gbc.insets = new Insets(3, 2, 3, 2);
gbc.anchor = GridBagConstraints.WEST;
gbc.fill = GridBagConstraints.HORIZONTAL;
gbc.weightx = 0.0;
gbc.gridx = 0;
gbc.gridy = 0;
panel.add(lonLabel, gbc);
gbc.weightx = 1;
gbc.gridx++;
gbc.gridwidth = 1;
panel.add(lonBox, gbc);
gbc.weightx = 0.0;
gbc.gridx = 0;
gbc.gridy++;
gbc.gridwidth = 1;
panel.add(latLabel, gbc);
gbc.weightx = 1;
gbc.gridx++;
gbc.gridwidth = 1;
panel.add(latBox, gbc);
gbc.weightx = 0.0;
gbc.gridx = 0;
gbc.gridy++;
gbc.gridwidth = 1;
panel.add(maskLabel, gbc);
gbc.weightx = 1;
gbc.gridx++;
panel.add(validMaskField, gbc);
gbc.weightx = 0;
gbc.gridx++;
panel.add(exprDialogButton, gbc);
gbc.weightx = 0.0;
gbc.gridx = 0;
gbc.gridy++;
gbc.gridwidth = 1;
panel.add(radiusLabel, gbc);
gbc.weightx = 1;
gbc.gridx++;
gbc.gridwidth = 1;
gbc.fill = GridBagConstraints.NONE;
gbc.anchor = GridBagConstraints.EAST;
panel.add(radiusSpinner, gbc);
gbc.weightx = 0;
gbc.gridx++;
panel.add(new JLabel("pixels"), gbc);
setContent(panel);
}
private void invokeExpressionEditor() {
SnapApp snapApp = SnapApp.getDefault();
final Window window = SwingUtilities.getWindowAncestor(snapApp.getMainFrame());
final ExpressionPane pane = ProductExpressionPane.createBooleanExpressionPane(new Product[]{product},
product,
snapApp.getPreferencesPropertyMap());
pane.setCode(validMaskField.getText());
final int status = pane.showModalDialog(window, "Edit Valid Mask Expression");
if (status == ID_OK) {
validMaskField.setText(pane.getCode());
validMaskField.setCaretPosition(0);
}
}
private void doPreSelection(final JComboBox comboBox, final String toFind) {
final String bandToSelect = getBandNameContaining(toFind);
if (StringUtils.isNotNullAndNotEmpty(bandToSelect)) {
comboBox.setSelectedItem(bandToSelect);
}
}
private String getBandNameContaining(final String toFind) {
return Arrays.stream(bandNames).filter(s -> s.contains(toFind)).findFirst().orElseGet(() -> null);
}
private String findBandName(final String bandName) {
return Arrays.stream(bandNames).filter(s -> s.equals(bandName)).findFirst().orElseGet(() -> null);
}
private static String createDefaultValidMask(final Product product) {
String validMask = null;
final String[] flagNames = product.getAllFlagNames();
final String invalidFlagName = "l1_flags.INVALID";
if (ArrayUtils.isMemberOf(invalidFlagName, flagNames)) {
validMask = "NOT " + invalidFlagName;
}
return validMask;
}
}
private static class UndoableAttachGeoCoding<T extends BasicPixelGeoCoding> extends AbstractUndoableEdit {
private Product product;
private T pixelGeoCoding;
public UndoableAttachGeoCoding(Product product, T pixelGeoCoding) {
Assert.notNull(product, "product");
Assert.notNull(pixelGeoCoding, "pixelGeoCoding");
this.product = product;
this.pixelGeoCoding = pixelGeoCoding;
}
@Override
public String getPresentationName() {
return Bundle.CTL_AttachPixelGeoCodingDialogTitle();
}
@Override
public void undo() throws CannotUndoException {
super.undo();
if (product.getSceneGeoCoding() == pixelGeoCoding) {
product.setSceneGeoCoding(pixelGeoCoding.getPixelPosEstimator());
}
}
@Override
public void redo() throws CannotRedoException {
super.redo();
product.setSceneGeoCoding(pixelGeoCoding);
}
@Override
public void die() {
pixelGeoCoding = null;
product = null;
}
}
}