/*
* Copyright (C) 2010 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.ui;
import com.bc.ceres.swing.TableLayout;
import org.esa.snap.core.datamodel.Product;
import org.esa.snap.core.datamodel.RGBImageProfile;
import org.esa.snap.core.datamodel.RGBImageProfileManager;
import org.esa.snap.core.dataop.barithm.BandArithmetic;
import org.esa.snap.core.jexp.ParseException;
import org.esa.snap.core.util.ArrayUtils;
import org.esa.snap.core.util.Debug;
import org.esa.snap.core.util.PropertyMap;
import org.esa.snap.core.util.io.FileUtils;
import org.esa.snap.core.util.io.SnapFileFilter;
import org.esa.snap.ui.product.ProductExpressionPane;
import org.esa.snap.ui.tool.ToolButtonFactory;
import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.ComboBoxEditor;
import javax.swing.DefaultComboBoxModel;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JComboBox;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.JTextField;
import javax.swing.SwingUtilities;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.GridLayout;
import java.awt.Insets;
import java.awt.Window;
import java.awt.event.ActionEvent;
import java.awt.event.ItemEvent;
import java.awt.event.ItemListener;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class RGBImageProfilePane extends JPanel {
private static final boolean SHOW_ALPHA = false;
private String[] COLOR_COMP_NAMES = new String[]{
"Red", /*I18N*/
"Green", /*I18N*/
"Blue", /*I18N*/
"Alpha", /*I18N*/
};
public static final Font EXPRESSION_FONT = new Font("Courier", Font.PLAIN, 12);
private static Color okMsgColor = new Color(0, 128, 0);
private static Color warnMsgColor = new Color(128, 0, 0);
private PropertyMap preferences;
private Product product;
private final Product[] openedProducts;
private JComboBox<ProfileItem> profileBox;
private JComboBox[] rgbaExprBoxes;
private DefaultComboBoxModel<ProfileItem> profileModel;
private AbstractAction saveAsAction;
private AbstractAction deleteAction;
private boolean settingRgbaExpressions;
private File lastDir;
protected JCheckBox storeInProductCheck;
private JLabel referencedRastersAreCompatibleLabel;
public RGBImageProfilePane(PropertyMap preferences) {
this(preferences, null, null, null);
}
public RGBImageProfilePane(PropertyMap preferences, Product product,
final Product[] openedProducts, final int[] defaultBandIndices) {
this.preferences = preferences;
this.product = product;
this.openedProducts = openedProducts;
AbstractAction openAction = new AbstractAction() {
public void actionPerformed(ActionEvent e) {
performOpen();
}
};
openAction.putValue(Action.LARGE_ICON_KEY, UIUtils.loadImageIcon("icons/Open24.gif"));
openAction.putValue(Action.SHORT_DESCRIPTION, "Open an external RGB profile");
saveAsAction = new AbstractAction() {
public void actionPerformed(ActionEvent e) {
performSaveAs();
}
};
saveAsAction.putValue(Action.LARGE_ICON_KEY, UIUtils.loadImageIcon("icons/Save24.gif"));
saveAsAction.putValue(Action.SHORT_DESCRIPTION, "Save the RGB profile");
deleteAction = new AbstractAction() {
public void actionPerformed(ActionEvent e) {
performDelete();
}
};
deleteAction.putValue(Action.LARGE_ICON_KEY,
UIUtils.loadImageIcon("icons/Remove24.gif")); // todo - use the nicer "cross" icon
deleteAction.putValue(Action.SHORT_DESCRIPTION, "Delete the selected RGB profile");
JPanel p2 = new JPanel(new GridLayout(1, 3, 2, 2));
p2.add(ToolButtonFactory.createButton(openAction, false));
p2.add(ToolButtonFactory.createButton(saveAsAction, false));
p2.add(ToolButtonFactory.createButton(deleteAction, false));
profileModel = new DefaultComboBoxModel<>();
profileBox = new JComboBox<>(profileModel);
profileBox.addItemListener(new ProfileSelectionHandler());
profileBox.setEditable(false);
profileBox.setName("profileBox");
setPreferredWidth(profileBox, 200);
storeInProductCheck = new JCheckBox();
storeInProductCheck.setText("Store RGB channels as virtual bands in current product");
storeInProductCheck.setSelected(false);
storeInProductCheck.setVisible(this.product != null);
storeInProductCheck.setName("storeInProductCheck");
final String[] bandNames;
if (this.product != null) {
bandNames = this.product.getBandNames();
} else {
bandNames = new String[0];
}
rgbaExprBoxes = new JComboBox[4];
for (int i = 0; i < rgbaExprBoxes.length; i++) {
rgbaExprBoxes[i] = createRgbaBox(bandNames);
rgbaExprBoxes[i].setName("rgbExprBox_" + i);
}
JPanel p1 = new JPanel(new BorderLayout(2, 2));
p1.add(new JLabel("Profile: "), BorderLayout.NORTH);
p1.add(profileBox, BorderLayout.CENTER);
p1.add(p2, BorderLayout.EAST);
JPanel p3 = new JPanel(new GridBagLayout());
final GridBagConstraints c3 = new GridBagConstraints();
c3.anchor = GridBagConstraints.WEST;
c3.fill = GridBagConstraints.HORIZONTAL;
c3.insets = new Insets(2, 2, 2, 2);
final int n = SHOW_ALPHA ? 4 : 3;
for (int i = 0; i < n; i++) {
c3.gridy = i;
addColorComponentRow(p3, c3, i);
}
referencedRastersAreCompatibleLabel = new JLabel();
TableLayout layout = new TableLayout(1);
layout.setTableFill(TableLayout.Fill.BOTH);
layout.setTableWeightX(1.0);
layout.setRowWeightY(3, 1.0);
layout.setTablePadding(10, 10);
setLayout(layout);
add(p1);
add(p3);
layout.setCellFill(2, 0, TableLayout.Fill.NONE);
layout.setCellAnchor(2, 0, TableLayout.Anchor.NORTHEAST);
add(referencedRastersAreCompatibleLabel);
add(storeInProductCheck);
add(layout.createVerticalSpacer());
final RGBImageProfile[] registeredProfiles = RGBImageProfileManager.getInstance().getAllProfiles();
addProfiles(registeredProfiles);
if (this.product != null) {
final RGBImageProfile productProfile = RGBImageProfile.getCurrentProfile(this.product);
if (productProfile.isValid()) {
final RGBImageProfile similarProfile = findMatchingProfile(productProfile);
if (similarProfile != null) {
selectProfile(similarProfile);
} else {
addNewProfile(productProfile);
selectProfile(productProfile);
}
} else {
List<RGBImageProfile> selectableProfiles = new ArrayList<>();
for (int i = 0; i < profileModel.getSize(); i++) {
selectableProfiles.add(profileModel.getElementAt(i).getProfile());
}
RGBImageProfile[] selectableProfileArray = selectableProfiles.toArray(new RGBImageProfile[selectableProfiles.size()]);
RGBImageProfile profile = findProfileForProductPattern(selectableProfileArray, product);
if (profile != null) {
selectProfile(profile);
}
}
}
setRgbaExpressionsFromSelectedProfile();
if (profileModel.getSelectedItem() == null) {
// default
if (defaultBandIndices != null && defaultBandIndices.length > 0) {
for (int i = 0; i < defaultBandIndices.length; ++i) {
rgbaExprBoxes[i].setSelectedIndex(defaultBandIndices[i]);
}
}
}
}
public Product getProduct() {
return product;
}
public void dispose() {
preferences = null;
product = null;
profileModel.removeAllElements();
profileModel = null;
profileBox = null;
saveAsAction = null;
deleteAction = null;
for (int i = 0; i < rgbaExprBoxes.length; i++) {
rgbaExprBoxes[i] = null;
}
rgbaExprBoxes = null;
}
public boolean getStoreProfileInProduct() {
return storeInProductCheck.isSelected();
}
/**
* Gets the selected RGB-image profile if any.
*
* @return the selected profile, can be null
* @see #getRgbaExpressions()
*/
public RGBImageProfile getSelectedProfile() {
final ProfileItem profileItem = getSelectedProfileItem();
return profileItem != null ? profileItem.getProfile() : null;
}
/**
* Gets the selected RGB expressions as array of 3 strings.
*
* @return the selected RGB expressions, never null
* @see #getSelectedProfile()
*/
public String[] getRgbExpressions() {
return new String[]{
getExpression(0),
getExpression(1),
getExpression(2),
};
}
/**
* Gets the selected RGBA expressions as array of 4 strings.
*
* @return the selected RGBA expressions, never null
* @see #getSelectedProfile()
*/
public String[] getRgbaExpressions() {
return new String[]{
getExpression(0),
getExpression(1),
getExpression(2),
getExpression(3),
};
}
public void addProfiles(RGBImageProfile[] profiles) {
for (RGBImageProfile profile : profiles) {
addNewProfile(profile);
}
setRgbaExpressionsFromSelectedProfile();
}
public RGBImageProfile findMatchingProfile(RGBImageProfile profile) {
// search in internal profiles first...
RGBImageProfile matchingProfile = findMatchingProfile(profile, true);
if (matchingProfile == null) {
// ...then in non-internal profiles
matchingProfile = findMatchingProfile(profile, false);
}
return matchingProfile;
}
public void selectProfile(RGBImageProfile profile) {
profileModel.setSelectedItem(new ProfileItem(profile));
}
public boolean showDialog(Window parent, String title, String helpId) {
ModalDialog modalDialog = new ModalDialog(parent, title, ModalDialog.ID_OK_CANCEL_HELP, helpId);
modalDialog.setContent(this);
final int status = modalDialog.show();
modalDialog.getJDialog().dispose();
return status == ModalDialog.ID_OK;
}
private String getExpression(int i) {
return ((JTextField) rgbaExprBoxes[i].getEditor().getEditorComponent()).getText().trim();
}
private void setExpression(int i, String expression) {
rgbaExprBoxes[i].setSelectedItem(expression);
}
private void performOpen() {
final SnapFileChooser snapFileChooser = new SnapFileChooser(getProfilesDir());
snapFileChooser.setFileFilter(
new SnapFileFilter("RGB-PROFILE", RGBImageProfile.FILENAME_EXTENSION, "RGB-Image Profile Files"));
final int status = snapFileChooser.showOpenDialog(this);
if (snapFileChooser.getSelectedFile() == null) {
return;
}
final File file = snapFileChooser.getSelectedFile();
lastDir = file.getParentFile();
if (status != SnapFileChooser.APPROVE_OPTION) {
return;
}
final RGBImageProfile profile;
try {
profile = RGBImageProfile.loadProfile(file);
} catch (IOException e) {
AbstractDialog.showErrorDialog(this,
String.format("Failed to open RGB-profile '%s':\n%s", file.getName(), e.getMessage()),
"Open RGB-Image Profile");
return;
}
if (profile == null) {
AbstractDialog.showErrorDialog(this,
String.format("Invalid RGB-Profile '%s'.", file.getName()),
"Open RGB-Image Profile");
return;
}
RGBImageProfileManager.getInstance().addProfile(profile);
if (product != null && !profile.isApplicableTo(product)) {
AbstractDialog.showErrorDialog(this,
String.format("The selected RGB-Profile '%s'\nis not applicable to the current product.",
profile.getName()),
"Open RGB-Image Profile");
return;
}
addNewProfile(profile);
}
private void performSaveAs() {
File file = promptForSaveFile();
if (file == null) {
return;
}
RGBImageProfile profile = new RGBImageProfile(FileUtils.getFilenameWithoutExtension(file),
getRgbaExpressions());
try {
profile.store(file);
} catch (IOException e) {
AbstractDialog.showErrorDialog(this,
"Failed to save RGB-profile '" + file.getName() + "':\n"
+ e.getMessage(),
"Open RGB-Image Profile");
return;
}
RGBImageProfileManager.getInstance().addProfile(profile);
addNewProfile(profile);
}
private File promptForSaveFile() {
final SnapFileChooser snapFileChooser = new SnapFileChooser(getProfilesDir());
snapFileChooser.setFileFilter(new SnapFileFilter("RGB-PROFILE", ".rgb", "RGB-Image Profile Files"));
File selectedFile;
while (true) {
final int status = snapFileChooser.showSaveDialog(this);
if (snapFileChooser.getSelectedFile() == null) {
selectedFile = null;
break;
}
selectedFile = snapFileChooser.getSelectedFile();
lastDir = selectedFile.getParentFile();
if (status != SnapFileChooser.APPROVE_OPTION) {
selectedFile = null;
break;
}
if (selectedFile.exists()) {
final int answer = JOptionPane.showConfirmDialog(RGBImageProfilePane.this,
"The file '" + selectedFile.getName()
+ "' already exists.\n" +
"So you really want to overwrite it?",
"Safe RGB-Profile As",
JOptionPane.YES_NO_CANCEL_OPTION);
if (answer == JOptionPane.CANCEL_OPTION) {
selectedFile = null;
break;
}
if (answer == JOptionPane.YES_OPTION) {
break;
}
} else {
break;
}
}
return selectedFile;
}
private void performDelete() {
final ProfileItem selectedProfileItem = getSelectedProfileItem();
if (selectedProfileItem != null && !selectedProfileItem.getProfile().isInternal()) {
profileModel.removeElement(selectedProfileItem);
}
}
private File getProfilesDir() {
if (lastDir != null) {
return lastDir;
} else {
return RGBImageProfileManager.getProfilesDir();
}
}
private void addNewProfile(RGBImageProfile profile) {
if (product != null && !profile.isApplicableTo(product)) {
return;
}
final ProfileItem profileItem = new ProfileItem(profile);
final int index = profileModel.getIndexOf(profileItem);
if (index == -1) {
profileModel.addElement(profileItem);
}
profileModel.setSelectedItem(profileItem);
}
private void setRgbaExpressionsFromSelectedProfile() {
settingRgbaExpressions = true;
try {
final ProfileItem profileItem = getSelectedProfileItem();
if (profileItem != null) {
final String[] rgbaExpressions = profileItem.getProfile().getRgbaExpressions();
for (int i = 0; i < rgbaExprBoxes.length; i++) {
setExpression(i, rgbaExpressions[i]);
}
} else {
for (int i = 0; i < rgbaExprBoxes.length; i++) {
setExpression(i, "");
}
}
} finally {
settingRgbaExpressions = false;
}
updateUIState();
}
private ProfileItem getSelectedProfileItem() {
return (ProfileItem) profileBox.getSelectedItem();
}
private void addColorComponentRow(JPanel p3, final GridBagConstraints constraints, final int index) {
final JButton editorButton = new JButton("...");
editorButton.addActionListener(e -> invokeExpressionEditor(index));
final Dimension preferredSize = rgbaExprBoxes[index].getPreferredSize();
editorButton.setPreferredSize(new Dimension(preferredSize.height, preferredSize.height));
constraints.gridy = index;
constraints.gridx = 0;
constraints.weightx = 0;
p3.add(new JLabel(getComponentName(index) + ": "), constraints);
constraints.gridx = 1;
constraints.weightx = 1;
p3.add(rgbaExprBoxes[index], constraints);
constraints.gridx = 2;
constraints.weightx = 0;
p3.add(editorButton, constraints);
}
protected String getComponentName(final int index) {
return COLOR_COMP_NAMES[index];
}
private void invokeExpressionEditor(final int colorIndex) {
final Window window = SwingUtilities.getWindowAncestor(this);
final String title = "Edit " + getComponentName(colorIndex) + " Expression";
if (product != null) {
final ExpressionPane pane;
final Product[] products = getCompatibleProducts(product, openedProducts);
pane = ProductExpressionPane.createGeneralExpressionPane(products, product, preferences);
pane.setCode(getExpression(colorIndex));
int status = pane.showModalDialog(window, title);
if (status == ModalDialog.ID_OK) {
setExpression(colorIndex, pane.getCode());
}
} else {
final JTextArea textArea = new JTextArea(8, 48);
textArea.setFont(EXPRESSION_FONT);
textArea.setLineWrap(true);
textArea.setWrapStyleWord(true);
textArea.setText(getExpression(colorIndex));
final ModalDialog modalDialog = new ModalDialog(window, title, ModalDialog.ID_OK_CANCEL, "");
final JPanel panel = new JPanel(new BorderLayout(2, 2));
panel.add(new JLabel("Expression:"), BorderLayout.NORTH);
panel.add(new JScrollPane(textArea), BorderLayout.CENTER);
modalDialog.setContent(panel);
final int status = modalDialog.show();
if (status == ModalDialog.ID_OK) {
setExpression(colorIndex, textArea.getText());
}
}
}
private static Product[] getCompatibleProducts(final Product targetProduct, final Product[] productsList) {
final List<Product> compatibleProducts = new ArrayList<>(1);
compatibleProducts.add(targetProduct);
final float geolocationEps = 180;
Debug.trace("BandMathsDialog.geolocationEps = " + geolocationEps);
Debug.trace("BandMathsDialog.getCompatibleProducts:");
Debug.trace(" comparing: " + targetProduct.getName());
if (productsList != null) {
for (final Product product : productsList) {
if (targetProduct != product) {
Debug.trace(" with: " + product.getDisplayName());
final boolean isCompatibleProduct = targetProduct.isCompatibleProduct(product, geolocationEps);
Debug.trace(" result: " + isCompatibleProduct);
if (isCompatibleProduct) {
compatibleProducts.add(product);
}
}
}
}
return compatibleProducts.toArray(new Product[compatibleProducts.size()]);
}
private JComboBox createRgbaBox(String[] suggestions) {
final JComboBox<String> comboBox = new JComboBox<>(suggestions);
setPreferredWidth(comboBox, 320);
comboBox.setEditable(true);
final ComboBoxEditor editor = comboBox.getEditor();
final JTextField textField = (JTextField) editor.getEditorComponent();
textField.setFont(EXPRESSION_FONT);
textField.getDocument().addDocumentListener(new DocumentListener() {
public void insertUpdate(DocumentEvent e) {
onRgbaExpressionChanged();
}
public void removeUpdate(DocumentEvent e) {
onRgbaExpressionChanged();
}
public void changedUpdate(DocumentEvent e) {
onRgbaExpressionChanged();
}
});
return comboBox;
}
private void onRgbaExpressionChanged() {
if (settingRgbaExpressions) {
return;
}
final ProfileItem profileItem = getSelectedProfileItem();
if (profileItem != null) {
if (isSelectedProfileModified()) {
profileBox.revalidate();
profileBox.repaint();
}
}
final String[] rgbaExpressions = getRgbaExpressions();
final int defaultProductIndex = ArrayUtils.getElementIndex(product, openedProducts);
try {
if (!BandArithmetic.areRastersEqualInSize(openedProducts,
defaultProductIndex, rgbaExpressions)) {
referencedRastersAreCompatibleLabel.setText("Referenced rasters are not of the same size");
referencedRastersAreCompatibleLabel.setForeground(warnMsgColor);
} else {
referencedRastersAreCompatibleLabel.setText("Expressions are valid");
referencedRastersAreCompatibleLabel.setForeground(okMsgColor);
}
} catch (ParseException e) {
referencedRastersAreCompatibleLabel.setText("Expressions are invalid");
referencedRastersAreCompatibleLabel.setForeground(warnMsgColor);
}
updateUIState();
}
private boolean isSelectedProfileModified() {
final ProfileItem profileItem = getSelectedProfileItem();
final String[] profileRgbaExpressions = profileItem.getProfile().getRgbaExpressions();
final String[] userRgbaExpressions = getRgbaExpressions();
for (int i = 0; i < profileRgbaExpressions.length; i++) {
final String userRgbaExpression = userRgbaExpressions[i];
final String profileRgbaExpression = profileRgbaExpressions[i];
if (!profileRgbaExpression.equals(userRgbaExpression)) {
return true;
}
}
return false;
}
private void updateUIState() {
final ProfileItem profileItem = getSelectedProfileItem();
if (profileItem != null) {
saveAsAction.setEnabled(true);
deleteAction.setEnabled(!profileItem.getProfile().isInternal());
} else {
saveAsAction.setEnabled(isAtLeastOneColorExpressionSet());
deleteAction.setEnabled(false);
}
}
private boolean isAtLeastOneColorExpressionSet() {
final JComboBox[] rgbaExprBoxes = this.rgbaExprBoxes;
for (int i = 0; i < 3; i++) {
JComboBox rgbaExprBox = rgbaExprBoxes[i];
final Object selectedItem = rgbaExprBox.getSelectedItem();
if (selectedItem != null && !selectedItem.toString().trim().equals("")) {
return true;
}
}
return false;
}
private void setPreferredWidth(final JComboBox comboBox, final int width) {
final Dimension preferredSize = comboBox.getPreferredSize();
comboBox.setPreferredSize(new Dimension(width, preferredSize.height));
}
public static RGBImageProfile findProfileForProductPattern(RGBImageProfile[] rgbImageProfiles, Product product) {
if (rgbImageProfiles.length == 0) {
return null;
}
String productType = product.getProductType();
String productName = product.getName();
String productDesc = product.getDescription();
RGBImageProfile bestProfile = rgbImageProfiles[0];
int bestMatchScore = 0;
for (RGBImageProfile rgbImageProfile : rgbImageProfiles) {
String[] pattern = rgbImageProfile.getPattern();
if (pattern == null) {
continue;
}
boolean productTypeMatches = matches(productType, pattern[0]);
boolean productNameMatches = matches(productName, pattern[1]);
boolean productDescMatches = matches(productDesc, pattern[2]);
int currentMatchScore = (productTypeMatches ? 100 : 0) + (productNameMatches ? 10 : 0) + (productDescMatches ? 1 : 0);
if (currentMatchScore > bestMatchScore) {
bestProfile = rgbImageProfile;
bestMatchScore = currentMatchScore;
}
}
return bestProfile;
}
private static boolean matches(String textValue, String pattern) {
return textValue != null
&& pattern != null
&& textValue.matches(pattern.replace("*", ".*").replace("?", "."));
}
private class ProfileItem {
private RGBImageProfile _profile;
public ProfileItem(RGBImageProfile profile) {
_profile = profile;
}
public RGBImageProfile getProfile() {
return _profile;
}
@Override
public int hashCode() {
return getProfile().hashCode();
}
@Override
public boolean equals(Object obj) {
if (obj == this) {
return true;
} else if (obj instanceof ProfileItem) {
ProfileItem profileItem = (ProfileItem) obj;
return getProfile().equals(profileItem.getProfile());
}
return false;
}
@Override
public String toString() {
String name = _profile.getName().replace('_', ' ');
if (getSelectedProfileItem().equals(this) && isSelectedProfileModified()) {
name += " (modified)";
}
return name;
}
}
private class ProfileSelectionHandler implements ItemListener {
public void itemStateChanged(ItemEvent e) {
setRgbaExpressionsFromSelectedProfile();
}
}
public RGBImageProfile findMatchingProfile(RGBImageProfile profile, boolean internal) {
final int size = profileModel.getSize();
for (int i = 0; i < size; i++) {
final ProfileItem item = profileModel.getElementAt(i);
final RGBImageProfile knownProfile = item.getProfile();
if (knownProfile.isInternal() == internal
&& Arrays.equals(profile.getRgbExpressions(), knownProfile.getRgbExpressions())) {
return knownProfile;
}
}
return null;
}
}