package org.esa.snap.rcp.magicwand; import com.bc.ceres.binding.PropertyContainer; import com.bc.ceres.swing.TableLayout; import com.bc.ceres.swing.binding.BindingContext; import com.bc.ceres.swing.undo.support.DefaultUndoContext; import com.thoughtworks.xstream.XStream; import org.esa.snap.core.datamodel.Band; import org.esa.snap.core.datamodel.Product; import org.esa.snap.core.util.io.FileUtils; import org.esa.snap.rcp.SnapApp; import org.esa.snap.rcp.util.Dialogs; import org.esa.snap.tango.TangoIcons; import org.esa.snap.ui.AbstractDialog; import org.esa.snap.ui.ModalDialog; import org.esa.snap.ui.product.BandChooser; import org.esa.snap.ui.product.ProductSceneView; import org.esa.snap.ui.tool.ToolButtonFactory; import org.openide.util.ImageUtilities; import javax.swing.AbstractButton; import javax.swing.ButtonGroup; import javax.swing.ImageIcon; import javax.swing.JCheckBox; import javax.swing.JFileChooser; import javax.swing.JLabel; import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JRadioButton; import javax.swing.JSlider; import javax.swing.JTextField; import javax.swing.border.EmptyBorder; import java.awt.BorderLayout; import java.awt.Color; import java.awt.Component; import java.awt.GridLayout; import java.awt.Insets; import java.beans.PropertyChangeListener; import java.io.File; import java.io.FileWriter; import java.io.IOException; import java.text.MessageFormat; import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.prefs.Preferences; import static com.bc.ceres.swing.TableLayout.*; /** * @author Norman Fomferra * @since BEAM 4.10 */ class MagicWandForm { public static final int TOLERANCE_SLIDER_RESOLUTION = 1000; public static final String PREFERENCES_KEY_LAST_DIR = "beam.magicWandTool.lastDir"; private MagicWandInteractor interactor; private JSlider toleranceSlider; boolean adjustingSlider; private DefaultUndoContext undoContext; private AbstractButton redoButton; private AbstractButton undoButton; private BindingContext bindingContext; private File settingsFile; private JLabel infoLabel; private AbstractButton minusButton; private AbstractButton plusButton; private AbstractButton clearButton; private AbstractButton saveButton; MagicWandForm(MagicWandInteractor interactor) { this.interactor = interactor; } public File getSettingsFile() { return settingsFile; } public BindingContext getBindingContext() { return bindingContext; } public JPanel createPanel() { undoContext = new DefaultUndoContext(this); interactor.setUndoContext(undoContext); bindingContext = new BindingContext(PropertyContainer.createObjectBacked(interactor.getModel())); bindingContext.addPropertyChangeListener("tolerance", evt -> { adjustSlider(); interactor.getModel().fireModelChanged(true); }); infoLabel = new JLabel(); infoLabel.setForeground(Color.DARK_GRAY); JLabel toleranceLabel = new JLabel("Tolerance:"); toleranceLabel.setToolTipText("Sets the maximum Euclidian distance tolerated"); JTextField toleranceField = new JTextField(10); bindingContext.bind("tolerance", toleranceField); toleranceField.setText(String.valueOf(interactor.getModel().getTolerance())); toleranceSlider = new JSlider(0, TOLERANCE_SLIDER_RESOLUTION); toleranceSlider.setSnapToTicks(false); toleranceSlider.setPaintTicks(false); toleranceSlider.setPaintLabels(false); toleranceSlider.addChangeListener(e -> { if (!adjustingSlider) { int sliderValue = toleranceSlider.getValue(); bindingContext.getPropertySet().setValue("tolerance", sliderValueToTolerance(sliderValue)); } }); JTextField minToleranceField = new JTextField(4); JTextField maxToleranceField = new JTextField(4); bindingContext.bind("minTolerance", minToleranceField); bindingContext.bind("maxTolerance", maxToleranceField); final PropertyChangeListener minMaxToleranceListener = evt -> adjustSlider(); bindingContext.addPropertyChangeListener("minTolerance", minMaxToleranceListener); bindingContext.addPropertyChangeListener("maxTolerance", minMaxToleranceListener); JPanel toleranceSliderPanel = new JPanel(new BorderLayout(2, 2)); toleranceSliderPanel.add(minToleranceField, BorderLayout.WEST); toleranceSliderPanel.add(toleranceSlider, BorderLayout.CENTER); toleranceSliderPanel.add(maxToleranceField, BorderLayout.EAST); JCheckBox normalizeCheckBox = new JCheckBox("Normalize spectra"); normalizeCheckBox.setToolTipText("Normalizes collected band sets by dividing their\n" + "individual values by the value of the first band"); bindingContext.bind("normalize", normalizeCheckBox); bindingContext.addPropertyChangeListener("normalize", evt -> interactor.getModel().fireModelChanged(true)); JLabel stLabel = new JLabel("Spectrum transformation:"); JRadioButton stButton1 = new JRadioButton("Integral"); JRadioButton stButton2 = new JRadioButton("Identity"); JRadioButton stButton3 = new JRadioButton("Derivative"); ButtonGroup stGroup = new ButtonGroup(); stGroup.add(stButton1); stGroup.add(stButton2); stGroup.add(stButton3); stButton1.setToolTipText("<html>Pixel similarity comparison is performed<br>" + "on the sums of subsequent band values"); stButton2.setToolTipText("<html>Pixel similarity comparison is performed<br>" + "on the original or normalised band values"); stButton3.setToolTipText("<html>Pixel similarity comparison is performed<br>" + "on the differences of subsequent band values"); bindingContext.bind("spectrumTransform", stGroup); bindingContext.addPropertyChangeListener("spectrumTransform", evt -> interactor.getModel().fireModelChanged(true)); JLabel ptLabel = new JLabel("Similarity metric:"); JRadioButton ptButton1 = new JRadioButton("Distance"); JRadioButton ptButton2 = new JRadioButton("Average"); JRadioButton ptButton3 = new JRadioButton("Min-Max"); ptButton1.setToolTipText("<html>Tests if the minimum of Euclidian distances of a pixel to<br>" + "each collected bands set is below the threshold"); ptButton2.setToolTipText("<html>Tests if the Euclidian distances of a pixel to<br>" + "the average of all collected bands sets is below the threshold"); ptButton3.setToolTipText("<html>Tests if a pixel is within the min/max limits<br>" + "of collected bands plus/minus tolerance"); ButtonGroup ptGroup = new ButtonGroup(); ptGroup.add(ptButton1); ptGroup.add(ptButton2); ptGroup.add(ptButton3); bindingContext.bind("pixelTest", ptGroup); bindingContext.addPropertyChangeListener("pixelTest", evt -> interactor.getModel().fireModelChanged(true)); plusButton = createToggleButton(TangoIcons.actions_list_add(TangoIcons.Res.R16)); plusButton.setToolTipText("<html>Switches to pick mode 'plus':<br>" + "collect spectra used for inclusion"); plusButton.addActionListener(e -> { if (interactor.getModel().getPickMode() != MagicWandModel.PickMode.PLUS) { interactor.getModel().setPickMode(MagicWandModel.PickMode.PLUS); } else { interactor.getModel().setPickMode(MagicWandModel.PickMode.SINGLE); } }); minusButton = createToggleButton(TangoIcons.actions_list_remove(TangoIcons.Res.R16)); minusButton.setToolTipText("<html>Switches to pick mode 'minus':<br>" + "collect spectra used for exclusion."); minusButton.addActionListener(e -> { if (interactor.getModel().getPickMode() != MagicWandModel.PickMode.MINUS) { interactor.getModel().setPickMode(MagicWandModel.PickMode.MINUS); } else { interactor.getModel().setPickMode(MagicWandModel.PickMode.SINGLE); } }); bindingContext.addPropertyChangeListener("pickMode", evt -> interactor.getModel().fireModelChanged(false)); final AbstractButton newButton = createButton(TangoIcons.actions_document_new(TangoIcons.R16)); newButton.setToolTipText("New settings"); newButton.addActionListener(e -> newSettings()); final AbstractButton openButton = createButton(TangoIcons.actions_document_open(TangoIcons.R16)); openButton.setToolTipText("Open settings"); openButton.addActionListener(e -> openSettings((Component) e.getSource())); saveButton = createButton(TangoIcons.actions_document_save(TangoIcons.R16)); saveButton.setToolTipText("Save settings"); saveButton.addActionListener(e -> saveSettings((Component) e.getSource(), settingsFile)); final AbstractButton saveAsButton = createButton(TangoIcons.actions_document_save_as(TangoIcons.R16)); saveAsButton.setToolTipText("Save settings as"); saveAsButton.addActionListener(e -> saveSettings((Component) e.getSource(), null)); undoButton = createButton(TangoIcons.actions_edit_undo(TangoIcons.R16)); undoButton.addActionListener(e -> { if (undoContext.canUndo()) { undoContext.undo(); } }); redoButton = createButton(TangoIcons.actions_edit_redo(TangoIcons.Res.R16)); redoButton.addActionListener(e -> { if (undoContext.canRedo()) { undoContext.redo(); } }); undoContext.addUndoableEditListener(e -> updateState()); clearButton = createButton(TangoIcons.actions_edit_clear(TangoIcons.Res.R16)); clearButton.setName("clearButton"); clearButton.setToolTipText("Removes all collected band ses."); /*I18N*/ clearButton.addActionListener(e -> clearSpectra()); AbstractButton filterButton = createButton(ImageUtilities.loadImageIcon("org/esa/snap/rcp/icons/Filter24.gif", false)); filterButton.setName("filterButton"); filterButton.setToolTipText("Select bands to included."); /*I18N*/ filterButton.addActionListener(e -> showBandChooser()); AbstractButton helpButton = createButton(TangoIcons.apps_help_browser(TangoIcons.Res.R16)); helpButton.setName("helpButton"); helpButton.setToolTipText("Help."); /*I18N*/ JPanel toolPanelN = new JPanel(new GridLayout(-1, 2)); toolPanelN.add(newButton); toolPanelN.add(openButton); toolPanelN.add(saveButton); toolPanelN.add(saveAsButton); toolPanelN.add(clearButton); toolPanelN.add(filterButton); toolPanelN.add(undoButton); toolPanelN.add(redoButton); toolPanelN.add(plusButton); toolPanelN.add(minusButton); JPanel toolPanelS = new JPanel(new GridLayout(-1, 2)); toolPanelS.add(new JLabel()); toolPanelS.add(helpButton); TableLayout tableLayout = new TableLayout(2); tableLayout.setTableAnchor(TableLayout.Anchor.WEST); tableLayout.setTableFill(TableLayout.Fill.HORIZONTAL); tableLayout.setTableWeightX(1.0); tableLayout.setTablePadding(2, 2); tableLayout.setCellColspan(1, 0, tableLayout.getColumnCount()); Insets insets = new Insets(2, 10, 2, 2); //tableLayout.setRowPadding(3, insets); //tableLayout.setRowPadding(4, insets); //tableLayout.setRowPadding(5, insets); tableLayout.setCellPadding(3, 0, insets); tableLayout.setCellPadding(4, 0, insets); tableLayout.setCellPadding(5, 0, insets); tableLayout.setCellPadding(6, 0, insets); tableLayout.setCellPadding(3, 1, insets); tableLayout.setCellPadding(4, 1, insets); tableLayout.setCellPadding(5, 1, insets); JPanel subPanel = new JPanel(tableLayout); subPanel.add(toleranceLabel, cell(0, 0)); subPanel.add(toleranceField, cell(0, 1)); subPanel.add(toleranceSliderPanel, cell(1, 0)); subPanel.add(stLabel, cell(2, 0)); subPanel.add(stButton1, cell(3, 0)); subPanel.add(stButton2, cell(4, 0)); subPanel.add(stButton3, cell(5, 0)); subPanel.add(ptLabel, cell(2, 1)); subPanel.add(ptButton1, cell(3, 1)); subPanel.add(ptButton2, cell(4, 1)); subPanel.add(ptButton3, cell(5, 1)); subPanel.add(normalizeCheckBox, cell(6, 0)); subPanel.add(infoLabel, cell(6, 1)); JPanel toolPanel = new JPanel(new BorderLayout(4, 4)); toolPanel.add(toolPanelN, BorderLayout.NORTH); toolPanel.add(new JLabel(), BorderLayout.CENTER); toolPanel.add(toolPanelS, BorderLayout.SOUTH); JPanel panel = new JPanel(new BorderLayout(4, 4)); panel.setBorder(new EmptyBorder(4, 4, 4, 4)); panel.add(subPanel, BorderLayout.CENTER); panel.add(toolPanel, BorderLayout.EAST); adjustSlider(); updateState(); return panel; } private void newSettings() { if (proceedWithUnsavedChanges()) { settingsFile = null; interactor.getModel().clearSpectra(); } } private void clearSpectra() { interactor.clearSpectra(); } private void openSettings(Component parent) { if (proceedWithUnsavedChanges()) { File settingsFile = getFile(parent, this.settingsFile, true); if (settingsFile == null) { return; } try { MagicWandModel model = (MagicWandModel) createXStream().fromXML(FileUtils.readText(settingsFile)); this.settingsFile = settingsFile; interactor.assignModel(model); undoContext.getUndoManager().discardAllEdits(); interactor.setModelModified(false); updateState(); } catch (IOException e) { String msg = MessageFormat.format("Failed to open settings:\n{0}", e.getMessage()); AbstractDialog.showErrorDialog(parent, msg, "I/O Error"); } } } private void saveSettings(Component parent, File settingsFile) { if (settingsFile == null) { settingsFile = getFile(parent, this.settingsFile, false); if (settingsFile == null) { return; } } try { try (FileWriter writer = new FileWriter(settingsFile)) { writer.write(createXStream().toXML(interactor.getModel())); } this.settingsFile = settingsFile; undoContext.getUndoManager().discardAllEdits(); interactor.setModelModified(false); interactor.updateForm(); } catch (IOException e) { String msg = MessageFormat.format("Failed to safe settings:\n{0}", e.getMessage()); AbstractDialog.showErrorDialog(parent, msg, "I/O Error"); } } private XStream createXStream() { XStream xStream = new XStream(); xStream.setClassLoader(MagicWandModel.class.getClassLoader()); xStream.alias("magicWandSettings", MagicWandModel.class); return xStream; } private static File getFile(Component parent, File file, boolean open) { JFileChooser fileChooser = new JFileChooser(Preferences.userRoot().get(PREFERENCES_KEY_LAST_DIR, System.getProperty("user.home"))); if (file != null) { fileChooser.setSelectedFile(file); } else { fileChooser.setSelectedFile(new File(fileChooser.getCurrentDirectory(), "magic-wand-settings.xml")); } while (true) { int resp = open ? fileChooser.showOpenDialog(parent) : fileChooser.showSaveDialog(parent); File settingsFile = fileChooser.getSelectedFile(); Preferences.userRoot().put(PREFERENCES_KEY_LAST_DIR, fileChooser.getCurrentDirectory().getPath()); if (resp != JFileChooser.APPROVE_OPTION) { return null; } if (open || !settingsFile.exists()) { return settingsFile; } String msg = MessageFormat.format("Settings file ''{0}'' already exists." + "\nOverwrite?", settingsFile.getName()); int resp2 = JOptionPane.showConfirmDialog(parent, msg, "File exists", JOptionPane.YES_NO_CANCEL_OPTION); if (resp2 == JOptionPane.YES_OPTION) { return settingsFile; } if (resp2 == JOptionPane.CANCEL_OPTION) { return null; } } } void updateState() { bindingContext.setComponentsEnabled("spectrumTransform", interactor.getModel().getBandCount() != 1); bindingContext.setComponentsEnabled("normalize", interactor.getModel().getBandCount() != 1); MagicWandModel model = interactor.getModel(); infoLabel.setText(String.format("%d(+), %d(-), %d bands", model.getPlusSpectrumCount(), model.getMinusSpectrumCount(), model.getBandCount())); plusButton.setSelected(model.getPickMode() == MagicWandModel.PickMode.PLUS); minusButton.setSelected(model.getPickMode() == MagicWandModel.PickMode.MINUS); saveButton.setEnabled(settingsFile != null && interactor.isModelModified()); clearButton.setEnabled(model.getSpectrumCount() > 0); undoButton.setEnabled(undoContext.canUndo()); redoButton.setEnabled(undoContext.canRedo()); } private void adjustSlider() { adjustingSlider = true; double tolerance = interactor.getModel().getTolerance(); toleranceSlider.setValue(toleranceToSliderValue(tolerance)); adjustingSlider = false; } private int toleranceToSliderValue(double tolerance) { MagicWandModel model = interactor.getModel(); double minTolerance = model.getMinTolerance(); double maxTolerance = model.getMaxTolerance(); return (int) Math.round(Math.abs(TOLERANCE_SLIDER_RESOLUTION * ((tolerance - minTolerance) / (maxTolerance - minTolerance)))); } private double sliderValueToTolerance(int sliderValue) { MagicWandModel model = interactor.getModel(); double minTolerance = model.getMinTolerance(); double maxTolerance = model.getMaxTolerance(); return minTolerance + sliderValue * (maxTolerance - minTolerance) / TOLERANCE_SLIDER_RESOLUTION; } private static AbstractButton createButton(ImageIcon imageIcon) { return ToolButtonFactory.createButton(imageIcon, false); } private static AbstractButton createToggleButton(ImageIcon imageIcon) { return ToolButtonFactory.createButton(imageIcon, true); } void showBandChooser() { final ProductSceneView view = SnapApp.getDefault().getSelectedProductSceneView(); if (view == null) { Dialogs.showInformation("Please select an image view first.", null); return; } Product product = view.getProduct(); Band[] bands = product.getBands(); if (bands.length == 0) { Dialogs.showInformation("No bands in product.", null); return; } Set<String> oldBandNames = new HashSet<>(interactor.getModel().getBandNames()); Set<Band> oldBandSet = new HashSet<>(); for (Band band : bands) { if (oldBandNames.contains(band.getName())) { oldBandSet.add(band); } } BandChooser bandChooser = new BandChooser(interactor.getOptionsWindow(), "Available Bands and Tie Point Grids", "", bands, oldBandSet.toArray(new Band[oldBandSet.size()]), product.getAutoGrouping(), true); if (bandChooser.show() == ModalDialog.ID_OK) { Band[] newBands = bandChooser.getSelectedBands(); Arrays.sort(newBands, new SpectralBandComparator()); List<String> newBandNames = new ArrayList<>(); for (Band newBand : newBands) { newBandNames.add(newBand.getName()); } if (!oldBandNames.containsAll(newBandNames) || !newBandNames.containsAll(oldBandNames)) { interactor.getModel().setBandNames(newBandNames); } } } private boolean proceedWithUnsavedChanges() { if (settingsFile != null && interactor.isModelModified()) { String msg = MessageFormat.format("You have unsaved changes." + "\nProceed anyway?", settingsFile.getName()); int resp = JOptionPane.showConfirmDialog(interactor.getOptionsWindow(), msg, "New Settings", JOptionPane.YES_NO_OPTION); return resp == JOptionPane.YES_OPTION; } return true; } }