/*
* Copyright (C) 2011 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.magicwand;
import com.bc.ceres.glayer.Layer;
import com.bc.ceres.glayer.LayerType;
import com.bc.ceres.glayer.support.AbstractLayerListener;
import com.bc.ceres.swing.figure.ViewportInteractor;
import com.bc.ceres.swing.undo.UndoContext;
import org.esa.snap.core.datamodel.Band;
import org.esa.snap.core.datamodel.Mask;
import org.esa.snap.core.datamodel.Product;
import org.esa.snap.core.datamodel.ProductNodeGroup;
import org.esa.snap.core.layer.MaskLayerType;
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.rcp.util.MultiSizeIssue;
import org.esa.snap.ui.UIUtils;
import org.esa.snap.ui.product.ProductSceneView;
import javax.swing.JDialog;
import javax.swing.undo.AbstractUndoableEdit;
import javax.swing.undo.CannotRedoException;
import javax.swing.undo.CannotUndoException;
import java.awt.Point;
import java.awt.Window;
import java.awt.event.MouseEvent;
import java.awt.geom.AffineTransform;
import java.awt.geom.NoninvertibleTransformException;
import java.awt.geom.Point2D;
import java.io.IOException;
import java.util.List;
/**
* An interactor that lets users create masks using a "magic wand".
* The mask comprises all pixels in the image that are "spectrally" close to the pixel that
* has been selected using the magic wand.
*
* @author Norman Fomferra
* @since BEAM 4.10
*/
public class MagicWandInteractor extends ViewportInteractor implements MagicWandModel.Listener {
private static final String DIALOG_TITLE = "Magic Wand Tool";
private JDialog optionsWindow;
private final MyLayerListener layerListener;
private MagicWandModel model;
private UndoContext undoContext;
private MagicWandForm form;
private boolean modelModified;
public MagicWandInteractor() {
layerListener = new MyLayerListener();
model = new MagicWandModel();
model.addListener(this);
}
public boolean isModelModified() {
return modelModified;
}
public void setModelModified(boolean modelModified) {
this.modelModified = modelModified;
}
@Override
public void modelChanged(MagicWandModel model, boolean recomputeMask) {
setModelModified(true);
if (recomputeMask) {
updateMask();
}
if (form != null) {
updateForm();
}
}
void updateForm() {
if (form.getSettingsFile() != null) {
optionsWindow.setTitle(DIALOG_TITLE + " - " + FileUtils.getFilenameWithoutExtension(form.getSettingsFile()));
} else {
optionsWindow.setTitle(DIALOG_TITLE);
}
form.getBindingContext().adjustComponents();
form.updateState();
}
public Window getOptionsWindow() {
return optionsWindow;
}
static double[] getSpectrum(List<Band> bands, int pixelX, int pixelY) throws IOException {
final double[] pixel = new double[1];
final double[] spectrum = new double[bands.size()];
for (int i = 0; i < bands.size(); i++) {
final Band band = bands.get(i);
band.readPixels(pixelX, pixelY, 1, 1, pixel, com.bc.ceres.core.ProgressMonitor.NULL);
final double value;
if (band.isPixelValid(pixelX, pixelY)) {
value = pixel[0];
} else {
value = Double.NaN;
}
spectrum[i] = value;
}
return spectrum;
}
@Override
public boolean activate() {
if (optionsWindow == null) {
optionsWindow = createOptionsWindow();
}
optionsWindow.setVisible(true);
ProductSceneView view = SnapApp.getDefault().getSelectedProductSceneView();
if (view != null) {
view.getRootLayer().addListener(layerListener);
}
return super.activate();
}
@Override
public void deactivate() {
super.deactivate();
if (optionsWindow != null) {
optionsWindow.setVisible(false);
}
ProductSceneView view = SnapApp.getDefault().getSelectedProductSceneView();
if (view != null) {
view.getRootLayer().removeListener(layerListener);
}
}
@Override
public void mouseClicked(MouseEvent event) {
final ProductSceneView view = SnapApp.getDefault().getSelectedProductSceneView();
if (view == null) {
return;
}
final Product product = view.getProduct();
if (MultiSizeIssue.isMultiSize(product)) {
MultiSizeIssue.maybeResample(product);
//as the following code requires an exact pixel location, nothing is done after resampling
return;
}
if (!ensureBandNamesSet(view, product)) {
return;
}
List<Band> bands = model.getBands(product);
if (bands == null) {
if (!handleInvalidBandFilter(view)) {
return;
}
bands = model.getBands(product);
if (bands == null) {
// Should not come here.
return;
}
}
Point pixelPos = getPixelPos(product, event);
if (pixelPos == null) {
return;
}
final double[] spectrum;
try {
spectrum = getSpectrum(bands, pixelPos.x, pixelPos.y);
} catch (IOException e1) {
return;
}
MagicWandModel oldModel = getModel().clone();
getModel().addSpectrum(spectrum);
MagicWandModel newModel = getModel().clone();
ensureMaskVisible(view);
undoContext.postEdit(new MyUndoableEdit(oldModel, newModel));
}
void clearSpectra() {
MagicWandModel oldModel = getModel().clone();
getModel().clearSpectra();
MagicWandModel newModel = getModel().clone();
undoContext.postEdit(new MyUndoableEdit(oldModel, newModel));
}
private void ensureMaskVisible(ProductSceneView view) {
Product product = view.getProduct();
ProductNodeGroup<Mask> overlayMaskGroup = view.getRaster().getOverlayMaskGroup();
Mask mask = overlayMaskGroup.getByDisplayName(MagicWandModel.MAGIC_WAND_MASK_NAME);
if (mask == null) {
mask = product.getMaskGroup().get(MagicWandModel.MAGIC_WAND_MASK_NAME);
if (mask != null) {
overlayMaskGroup.add(mask);
}
}
}
private boolean handleInvalidBandFilter(ProductSceneView view) {
Product product = view.getProduct();
Dialogs.Answer answer = Dialogs.requestDecision(DIALOG_TITLE,
"The currently selected band filter does not match\n" +
"the bands of the selected data product.\n\n" +
"Reset filter and use the ones of the selected product?",
false,
"reset_magic_wand_filter");
if (answer == Dialogs.Answer.YES) {
model.setBandNames();
return ensureBandNamesSet(view, product);
} else {
return false;
}
}
Point getPixelPos(Product product, MouseEvent event) {
final Point2D mp = toModelPoint(event);
final Point2D ip;
if (product.getSceneGeoCoding() != null) {
AffineTransform transform = Product.findImageToModelTransform(product.getSceneGeoCoding());
try {
ip = transform.inverseTransform(mp, null);
} catch (NoninvertibleTransformException e) {
Dialogs.showError(DIALOG_TITLE, "A geographic transformation problem occurred:\n" + e.getMessage());
return null;
}
} else {
ip = mp;
}
final int pixelX = (int) ip.getX();
final int pixelY = (int) ip.getY();
if (pixelX < 0
|| pixelY < 0
|| pixelX >= product.getSceneRasterWidth()
|| pixelY >= product.getSceneRasterHeight()) {
return null;
}
return new Point(pixelX, pixelY);
}
private boolean ensureBandNamesSet(ProductSceneView view, Product product) {
if (model.getBandCount() == 0) {
model.setSpectralBandNames(product);
}
if (model.getBandCount() == 0) {
model.setBandNames(view.getRaster().getName());
}
if (model.getBandCount() == 0) {
// It's actually hard to get here, because we have a selected image view...
Dialogs.showError(DIALOG_TITLE, "No bands selected.");
return false;
}
return true;
}
void updateMask() {
final ProductSceneView view = SnapApp.getDefault().getSelectedProductSceneView();
if (view != null) {
final Product product = view.getProduct();
updateMagicWandMask(product);
}
}
private void updateMagicWandMask(Product product) {
MagicWandModel.setMagicWandMask(product, getModel().createMaskExpression());
}
private JDialog createOptionsWindow() {
form = new MagicWandForm(this);
JDialog optionsWindow = new JDialog(SnapApp.getDefault().getMainFrame(), DIALOG_TITLE, false);
UIUtils.centerComponent(optionsWindow, SnapApp.getDefault().getMainFrame());
optionsWindow.getContentPane().add(form.createPanel());
optionsWindow.pack();
return optionsWindow;
}
public MagicWandModel getModel() {
return model;
}
public void setUndoContext(UndoContext undoContext) {
this.undoContext = undoContext;
}
void assignModel(MagicWandModel other) {
getModel().assign(other);
}
/**
* A layer listener that sets the layer for "magic_wand" mask
* visible, once it is added to the view's layer tree.
*/
private static class MyLayerListener extends AbstractLayerListener {
@Override
public void handleLayersAdded(Layer parentLayer, Layer[] childLayers) {
for (Layer childLayer : childLayers) {
LayerType layerType = childLayer.getLayerType();
if (layerType instanceof MaskLayerType) {
if (childLayer.getName().equals(MagicWandModel.MAGIC_WAND_MASK_NAME)) {
childLayer.setVisible(true);
}
}
}
}
}
private class MyUndoableEdit extends AbstractUndoableEdit {
private final MagicWandModel oldModel;
private final MagicWandModel newModel;
public MyUndoableEdit(MagicWandModel oldModel, MagicWandModel newModel) {
this.oldModel = oldModel;
this.newModel = newModel;
}
@Override
public void undo() throws CannotUndoException {
super.undo();
assignModel(oldModel);
}
@Override
public void redo() throws CannotRedoException {
super.redo();
assignModel(newModel);
}
@Override
public String getPresentationName() {
return "Modify magic wand mask";
}
}
}