/*
* Copyright (C) 2015 by Array Systems Computing Inc. http://www.array.ca
*
* 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.dem.rcp;
import com.bc.ceres.binding.Property;
import com.bc.ceres.binding.PropertyContainer;
import com.bc.ceres.binding.PropertyDescriptor;
import com.bc.ceres.binding.PropertySet;
import com.bc.ceres.binding.ValidationException;
import com.bc.ceres.binding.Validator;
import com.bc.ceres.binding.ValueSet;
import com.bc.ceres.glevel.support.AbstractMultiLevelSource;
import com.bc.ceres.glevel.support.DefaultMultiLevelImage;
import com.bc.ceres.swing.TableLayout;
import com.bc.ceres.swing.binding.BindingContext;
import com.bc.ceres.swing.binding.ComponentAdapter;
import org.esa.snap.core.datamodel.Band;
import org.esa.snap.core.datamodel.GeoCoding;
import org.esa.snap.core.datamodel.PixelPos;
import org.esa.snap.core.datamodel.Product;
import org.esa.snap.core.datamodel.ProductData;
import org.esa.snap.core.datamodel.ProductNode;
import org.esa.snap.core.dataop.dem.ElevationModel;
import org.esa.snap.core.dataop.dem.ElevationModelDescriptor;
import org.esa.snap.core.dataop.dem.ElevationModelRegistry;
import org.esa.snap.core.dataop.resamp.Resampling;
import org.esa.snap.core.dataop.resamp.ResamplingFactory;
import org.esa.snap.core.image.RasterDataNodeSampleOpImage;
import org.esa.snap.core.image.ResolutionLevel;
import org.esa.snap.dem.dataio.DEMFactory;
import org.esa.snap.engine_utilities.datamodel.Unit;
import org.esa.snap.rcp.SnapApp;
import org.esa.snap.rcp.util.Dialogs;
import org.esa.snap.ui.ModalDialog;
import org.openide.awt.ActionID;
import org.openide.awt.ActionReference;
import org.openide.awt.ActionReferences;
import org.openide.awt.ActionRegistration;
import org.openide.util.ContextAwareAction;
import org.openide.util.HelpCtx;
import org.openide.util.Lookup;
import org.openide.util.LookupEvent;
import org.openide.util.LookupListener;
import org.openide.util.NbBundle;
import org.openide.util.Utilities;
import org.openide.util.WeakListeners;
import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.JCheckBox;
import javax.swing.JComboBox;
import javax.swing.JComponent;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextField;
import javax.swing.ListSelectionModel;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import java.awt.event.ActionEvent;
import java.awt.image.RenderedImage;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.text.MessageFormat;
import java.util.Arrays;
import java.util.List;
@ActionID(
category = "Tools",
id = "AddElevationAction"
)
@ActionRegistration(
displayName = "#CTL_AddElevationAction_MenuText",
popupText = "#CTL_AddElevationAction_MenuText"
)
@ActionReferences({
@ActionReference(
path = "Menu/Raster/DEM Tools",
position = 250
),
@ActionReference(
path = "Shortcuts",
name = "D-E"
),
@ActionReference(
path = "Context/Product/Product",
position = 20
),
@ActionReference(
path = "Context/Product/RasterDataNode",
position = 10
),
})
@NbBundle.Messages({
"CTL_AddElevationAction_MenuText=Add Elevation Band",
"CTL_AddElevationAction_ShortDescription=Create a new elevation band from a DEM"
})
public class AddElevationAction extends AbstractAction implements ContextAwareAction, LookupListener, HelpCtx.Provider {
private static final String HELP_ID = "createElevation";
private final Lookup lkp;
private Product product;
public static final String DIALOG_TITLE = "Add Elevation Band";
public static final String DEFAULT_ELEVATION_BAND_NAME = "elevation";
public static final String DEFAULT_LATITUDE_BAND_NAME = "corr_latitude";
public static final String DEFAULT_LONGITUDE_BAND_NAME = "corr_longitude";
public AddElevationAction() {
this(Utilities.actionsGlobalContext());
}
public AddElevationAction(Lookup lkp) {
super(Bundle.CTL_AddElevationAction_MenuText());
this.lkp = lkp;
Lookup.Result<ProductNode> lkpContext = lkp.lookupResult(ProductNode.class);
lkpContext.addLookupListener(WeakListeners.create(LookupListener.class, this, lkpContext));
setEnableState();
putValue(Action.SHORT_DESCRIPTION, Bundle.CTL_AddElevationAction_ShortDescription());
}
@Override
public Action createContextAwareInstance(Lookup actionContext) {
return new AddElevationAction(actionContext);
}
@Override
public void resultChanged(LookupEvent ev) {
setEnableState();
}
private void setEnableState() {
ProductNode productNode = lkp.lookup(ProductNode.class);
boolean state = false;
if (productNode != null) {
product = productNode.getProduct();
state = product.getSceneGeoCoding() != null;
}
setEnabled(state);
}
@Override
public void actionPerformed(ActionEvent event) {
final DialogData dialogData = requestDialogData(product);
if (dialogData == null) {
return;
}
final String demName = DEMFactory.getProperDEMName(dialogData.demName);
final ElevationModelRegistry elevationModelRegistry = ElevationModelRegistry.getInstance();
final ElevationModelDescriptor demDescriptor = elevationModelRegistry.getDescriptor(demName);
if (demDescriptor == null) {
Dialogs.showError(DIALOG_TITLE, "The DEM '" + demName + "' is not supported.");
return;
}
Resampling resampling = Resampling.BILINEAR_INTERPOLATION;
if (dialogData.resamplingMethod != null) {
resampling = ResamplingFactory.createResampling(dialogData.resamplingMethod);
}
computeBands(product,
demDescriptor,
dialogData.outputElevationBand ? dialogData.elevationBandName : null,
resampling);
}
@Override
public HelpCtx getHelpCtx() {
return new HelpCtx(HELP_ID);
}
private static void computeBands(final Product product,
final ElevationModelDescriptor demDescriptor,
final String elevationBandName,
final Resampling resampling) {
final ElevationModel dem = demDescriptor.createDem(resampling);
if (elevationBandName != null) {
addElevationBand(product, dem, elevationBandName);
}
}
private static void addElevationBand(Product product, ElevationModel dem, String elevationBandName) {
final GeoCoding geoCoding = product.getSceneGeoCoding();
ElevationModelDescriptor demDescriptor = dem.getDescriptor();
final float noDataValue = dem.getDescriptor().getNoDataValue();
final Band elevationBand = product.addBand(elevationBandName, ProductData.TYPE_FLOAT32);
elevationBand.setNoDataValueUsed(true);
elevationBand.setNoDataValue(noDataValue);
elevationBand.setUnit(Unit.METERS);
elevationBand.setDescription(demDescriptor.getName());
elevationBand.setSourceImage(createElevationSourceImage(dem, geoCoding, elevationBand));
}
private static RenderedImage createElevationSourceImage(final ElevationModel dem, final GeoCoding geoCoding, final Band band) {
return new DefaultMultiLevelImage(new AbstractMultiLevelSource(band.createMultiLevelModel()) {
@Override
protected RenderedImage createImage(final int level) {
return new ElevationSourceImage(dem, geoCoding, band, ResolutionLevel.create(getModel(), level));
}
});
}
private static boolean isOrtorectifiable(Product product) {
return product.getNumBands() > 0 && product.getBandAt(0).canBeOrthorectified();
}
private DialogData requestDialogData(final Product product) {
boolean ortorectifiable = isOrtorectifiable(product);
String[] demNames = DEMFactory.getDEMNameList();
// sort the list
final List<String> sortedDEMNames = Arrays.asList(demNames);
java.util.Collections.sort(sortedDEMNames);
demNames = sortedDEMNames.toArray(new String[sortedDEMNames.size()]);
final DialogData dialogData = new DialogData("SRTM 3sec (Auto Download)", ResamplingFactory.BILINEAR_INTERPOLATION_NAME, ortorectifiable);
PropertySet propertySet = PropertyContainer.createObjectBacked(dialogData);
configureDemNameProperty(propertySet, "demName", demNames, "SRTM 3sec (Auto Download)");
configureDemNameProperty(propertySet, "resamplingMethod", ResamplingFactory.resamplingNames,
ResamplingFactory.BILINEAR_INTERPOLATION_NAME);
configureBandNameProperty(propertySet, "elevationBandName", product);
configureBandNameProperty(propertySet, "latitudeBandName", product);
configureBandNameProperty(propertySet, "longitudeBandName", product);
final BindingContext ctx = new BindingContext(propertySet);
JList demList = new JList();
demList.setVisibleRowCount(10);
ctx.bind("demName", new SingleSelectionListComponentAdapter(demList));
JTextField elevationBandNameField = new JTextField();
elevationBandNameField.setColumns(10);
ctx.bind("elevationBandName", elevationBandNameField);
JCheckBox outputDemCorrectedBandsChecker = new JCheckBox("Output DEM-corrected bands");
ctx.bind("outputDemCorrectedBands", outputDemCorrectedBandsChecker);
JLabel latitudeBandNameLabel = new JLabel("Latitude band name:");
JTextField latitudeBandNameField = new JTextField();
latitudeBandNameField.setEnabled(ortorectifiable);
ctx.bind("latitudeBandName", latitudeBandNameField).addComponent(latitudeBandNameLabel);
ctx.bindEnabledState("latitudeBandName", true, "outputGeoCodingBands", true);
JLabel longitudeBandNameLabel = new JLabel("Longitude band name:");
JTextField longitudeBandNameField = new JTextField();
longitudeBandNameField.setEnabled(ortorectifiable);
ctx.bind("longitudeBandName", longitudeBandNameField).addComponent(longitudeBandNameLabel);
ctx.bindEnabledState("longitudeBandName", true, "outputGeoCodingBands", true);
TableLayout tableLayout = new TableLayout(2);
tableLayout.setTableAnchor(TableLayout.Anchor.WEST);
tableLayout.setTableFill(TableLayout.Fill.HORIZONTAL);
tableLayout.setTablePadding(4, 4);
tableLayout.setCellColspan(0, 0, 2);
tableLayout.setCellColspan(1, 0, 2);
/* tableLayout.setCellColspan(3, 0, 2);
tableLayout.setCellWeightX(0, 0, 1.0);
tableLayout.setRowWeightX(1, 1.0);
tableLayout.setCellWeightX(2, 1, 1.0);
tableLayout.setCellWeightX(4, 1, 1.0);
tableLayout.setCellWeightX(5, 1, 1.0);
tableLayout.setCellPadding(4, 0, new Insets(0, 24, 0, 4));
tableLayout.setCellPadding(5, 0, new Insets(0, 24, 0, 4)); */
JPanel parameterPanel = new JPanel(tableLayout);
/*row 0*/
parameterPanel.add(new JLabel("Digital elevation model (DEM):"));
parameterPanel.add(new JScrollPane(demList));
/*row 1*/
parameterPanel.add(new JLabel("Resampling method:"));
final JComboBox resamplingCombo = new JComboBox(DEMFactory.getDEMResamplingMethods());
parameterPanel.add(resamplingCombo);
ctx.bind("resamplingMethod", resamplingCombo);
parameterPanel.add(new JLabel("Elevation band name:"));
parameterPanel.add(elevationBandNameField);
if (ortorectifiable) {
/*row 2*/
parameterPanel.add(outputDemCorrectedBandsChecker);
/*row 3*/
parameterPanel.add(latitudeBandNameLabel);
parameterPanel.add(latitudeBandNameField);
/*row 4*/
parameterPanel.add(longitudeBandNameLabel);
parameterPanel.add(longitudeBandNameField);
outputDemCorrectedBandsChecker.setSelected(ortorectifiable);
outputDemCorrectedBandsChecker.setEnabled(ortorectifiable);
}
final ModalDialog dialog = new ModalDialog(SnapApp.getDefault().getMainFrame(), DIALOG_TITLE, ModalDialog.ID_OK_CANCEL, HELP_ID);
dialog.setContent(parameterPanel);
if (dialog.show() == ModalDialog.ID_OK) {
return dialogData;
}
return null;
}
private static void configureDemNameProperty(PropertySet propertySet, String propertyName, String[] demNames, String defaultValue) {
PropertyDescriptor descriptor = propertySet.getProperty(propertyName).getDescriptor();
descriptor.setValueSet(new ValueSet(demNames));
descriptor.setDefaultValue(defaultValue);
descriptor.setNotNull(true);
descriptor.setNotEmpty(true);
}
private static void configureBandNameProperty(PropertySet propertySet, String propertyName, Product product) {
Property property = propertySet.getProperty(propertyName);
PropertyDescriptor descriptor = property.getDescriptor();
descriptor.setNotNull(true);
descriptor.setNotEmpty(true);
descriptor.setValidator(new BandNameValidator(product));
setValidBandName(property, product);
}
private static void setValidBandName(Property property, Product product) {
String bandName = (String) property.getValue();
String bandNameStub = bandName;
for (int i = 2; product.containsBand(bandName); i++) {
bandName = String.format("%s_%d", bandNameStub, i);
}
try {
property.setValue(bandName);
} catch (ValidationException e) {
throw new IllegalStateException(e);
}
}
private static class SingleSelectionListComponentAdapter extends ComponentAdapter implements ListSelectionListener, PropertyChangeListener {
private final JList list;
public SingleSelectionListComponentAdapter(JList list) {
this.list = list;
list.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
}
@Override
public JComponent[] getComponents() {
return new JComponent[]{list};
}
@Override
public void bindComponents() {
updateListModel();
getPropertyDescriptor().addAttributeChangeListener(this);
list.addListSelectionListener(this);
}
@Override
public void unbindComponents() {
getPropertyDescriptor().removeAttributeChangeListener(this);
list.removeListSelectionListener(this);
}
@Override
public void adjustComponents() {
Object value = getBinding().getPropertyValue();
if (value != null) {
list.setSelectedValue(value, true);
} else {
list.clearSelection();
}
}
@Override
public void propertyChange(PropertyChangeEvent evt) {
if (evt.getSource() == getPropertyDescriptor() && evt.getPropertyName().equals("valueSet")) {
updateListModel();
}
}
private PropertyDescriptor getPropertyDescriptor() {
return getBinding().getContext().getPropertySet().getDescriptor(getBinding().getPropertyName());
}
private void updateListModel() {
ValueSet valueSet = getPropertyDescriptor().getValueSet();
if (valueSet != null) {
list.setListData(valueSet.getItems());
adjustComponents();
}
}
@Override
public void valueChanged(ListSelectionEvent event) {
if (event.getValueIsAdjusting()) {
return;
}
if (getBinding().isAdjustingComponents()) {
return;
}
final Property property = getBinding().getContext().getPropertySet().getProperty(getBinding().getPropertyName());
Object selectedValue = list.getSelectedValue();
try {
property.setValue(selectedValue);
// Now model is in sync with UI
getBinding().clearProblem();
} catch (ValidationException e) {
getBinding().reportProblem(e);
}
}
}
private static class BandNameValidator implements Validator {
private final Product product;
public BandNameValidator(Product product) {
this.product = product;
}
@Override
public void validateValue(Property property, Object value) throws ValidationException {
final String bandName = value.toString().trim();
if (!ProductNode.isValidNodeName(bandName)) {
throw new ValidationException(MessageFormat.format("The band name ''{0}'' appears not to be valid.\n" +
"Please choose another one.",
bandName
));
} else if (product.containsBand(bandName)) {
throw new ValidationException(MessageFormat.format("The selected product already contains a band named ''{0}''.\n" +
"Please choose another one.",
bandName
));
}
}
}
private static class ElevationSourceImage extends RasterDataNodeSampleOpImage {
private final ElevationModel dem;
private final GeoCoding geoCoding;
private double noDataValue;
public ElevationSourceImage(ElevationModel dem, GeoCoding geoCoding, Band band, ResolutionLevel level) {
super(band, level);
this.dem = dem;
this.geoCoding = geoCoding;
noDataValue = band.getNoDataValue();
}
@Override
protected double computeSample(int sourceX, int sourceY) {
try {
return dem.getElevation(geoCoding.getGeoPos(new PixelPos(sourceX, sourceY), null));
} catch (Exception e) {
return noDataValue;
}
}
}
private static class DialogData {
String demName;
String resamplingMethod;
boolean outputElevationBand;
boolean outputDemCorrectedBands;
String elevationBandName = DEFAULT_ELEVATION_BAND_NAME;
String latitudeBandName = DEFAULT_LATITUDE_BAND_NAME;
String longitudeBandName = DEFAULT_LONGITUDE_BAND_NAME;
public DialogData(String demName, String resamplingMethod, boolean ortorectifiable) {
this.demName = demName;
this.resamplingMethod = resamplingMethod;
outputElevationBand = true;
outputDemCorrectedBands = ortorectifiable;
}
}
}