/*
* 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.actions.file.export;
import com.bc.ceres.core.ProgressMonitor;
import com.bc.ceres.swing.figure.Figure;
import com.bc.ceres.swing.figure.FigureSelection;
import com.bc.ceres.swing.figure.ShapeFigure;
import com.bc.ceres.swing.progress.DialogProgressMonitor;
import org.esa.snap.core.datamodel.Band;
import org.esa.snap.core.datamodel.GeoCoding;
import org.esa.snap.core.datamodel.GeoPos;
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.RasterDataNode;
import org.esa.snap.core.datamodel.TiePointGrid;
import org.esa.snap.core.datamodel.TransectProfileData;
import org.esa.snap.core.datamodel.TransectProfileDataBuilder;
import org.esa.snap.core.util.StringUtils;
import org.esa.snap.core.util.SystemUtils;
import org.esa.snap.core.util.io.FileUtils;
import org.esa.snap.core.util.io.SnapFileFilter;
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.SelectExportMethodDialog;
import org.esa.snap.ui.UIUtils;
import org.esa.snap.ui.product.ProductSceneView;
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.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.SwingWorker;
import java.awt.Dialog;
import java.awt.event.ActionEvent;
import java.awt.geom.Point2D;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.Date;
@ActionID(category = "File", id = "org.esa.snap.rcp.actions.file.export.ExportTransectPixelsAction" )
@ActionRegistration(
displayName = "#CTL_ExportTransectPixelsAction_MenuText",
popupText = "#CTL_ExportTransectPixelsAction_PopupText",
lazy = false
)
@ActionReferences({
@ActionReference(path = "Menu/File/Export/Other",position = 60 ),
@ActionReference(path = "Menu/Raster/Export", position = 200),
@ActionReference(path = "Context/Product/RasterDataNode", position = 50, separatorAfter = 55),
@ActionReference(path = "Context/ProductSceneView" , position = 40)
})
@NbBundle.Messages({
"CTL_ExportTransectPixelsAction_MenuText=Transect Pixels",
"CTL_ExportTransectPixelsAction_PopupText=Export Transect Pixels",
"CTL_ExportTransectPixelsAction_DialogTitle=Export Transect Pixels",
"CTL_ExportTransectPixelsAction_ShortDescription=Export Transect Pixels."
})
public class ExportTransectPixelsAction extends AbstractAction implements ContextAwareAction, LookupListener {
private static final String ERR_MSG_BASE = "Transect pixels cannot be exported:\n";
private final Lookup.Result<FigureSelection> result;
public ExportTransectPixelsAction() {
this(Utilities.actionsGlobalContext());
}
public ExportTransectPixelsAction(Lookup lkp) {
super(Bundle.CTL_ExportTransectPixelsAction_MenuText());
putValue("popupText", Bundle.CTL_ExportTransectPixelsAction_PopupText());
result = lkp.lookupResult(FigureSelection.class);
result.addLookupListener(WeakListeners.create(LookupListener.class, this, result));
updateEnableState(getCurrentFigureSelection());
}
/**
* Invoked when a command action is performed.
*
* @param event the command event
*/
@Override
public void actionPerformed(ActionEvent event) {
final ProductSceneView sceneView = SnapApp.getDefault().getSelectedProductSceneView();
if(sceneView != null && sceneView.getProduct().isMultiSize()) {
MultiSizeIssue.maybeResample(sceneView.getProduct());
//as the following code relies on current selections, nothing is done after resampling
} else {
exportTransectPixels();
}
}
@Override
public Action createContextAwareInstance(Lookup lkp) {
return new ExportTransectPixelsAction(lkp);
}
@Override
public void resultChanged(LookupEvent le) {
updateEnableState(getCurrentFigureSelection());
}
private void exportTransectPixels() {
// Get current VISAT view showing a product's band
final ProductSceneView view = SnapApp.getDefault().getSelectedProductSceneView();
if (view == null) {
return;
}
final FigureSelection selection = getCurrentFigureSelection();
// Get the displayed raster data node (band or tie-point grid)
final RasterDataNode raster = view.getRaster();
// Get the transect of the displayed raster data node
ShapeFigure transect = null;
if (selection.getFigureCount() > 0) {
Figure figure = selection.getFigure(0);
if (figure instanceof ShapeFigure) {
transect = (ShapeFigure) figure;
}
}
if (transect == null) {
Dialogs.showError(Bundle.CTL_ExportTransectPixelsAction_DialogTitle(),
ERR_MSG_BASE + "There is no transect defined in the selected band.");
return;
}
final TransectProfileData transectProfileData;
try {
transectProfileData = new TransectProfileDataBuilder()
.raster(raster)
.path(transect.getShape())
.build();
} catch (IOException e) {
Dialogs.showError(Bundle.CTL_ExportTransectPixelsAction_DialogTitle(),
ERR_MSG_BASE + "An I/O error occurred:\n" + e.getMessage());
return;
}
// Compute total number of transect pixels
final int numTransectPixels = getNumTransectPixels(raster.getProduct(), transectProfileData);
String numPixelsText;
if (numTransectPixels == 1) {
numPixelsText = "One transect pixel will be exported.\n";
} else {
numPixelsText = numTransectPixels + " transect pixels will be exported.\n";
}
// Get export method from user
final String questionText = "How do you want to export the pixel values?\n";
final JCheckBox createHeaderBox = new JCheckBox("Create header");
final JCheckBox exportTiePointsBox = new JCheckBox("Export tie-points");
final JCheckBox exportWavelengthsAndSFBox = new JCheckBox("Export wavelengths + solar fluxes");
final int method = SelectExportMethodDialog.run(SnapApp.getDefault().getMainFrame(), getWindowTitle(),
questionText + numPixelsText, new JCheckBox[]{
createHeaderBox,
exportTiePointsBox,
exportWavelengthsAndSFBox
}, "exportTransectPixels");
final PrintWriter out;
final StringBuffer clipboardText;
final int initialBufferSize = 256000;
if (method == SelectExportMethodDialog.EXPORT_TO_CLIPBOARD) {
// Write into string buffer
final StringWriter stringWriter = new StringWriter(initialBufferSize);
out = new PrintWriter(stringWriter);
clipboardText = stringWriter.getBuffer();
} else if (method == SelectExportMethodDialog.EXPORT_TO_FILE) {
// Write into file, get file from user
final File file = promptForFile(createDefaultFileName(raster));
if (file == null) {
return; // Cancel
}
final FileWriter fileWriter;
try {
fileWriter = new FileWriter(file);
} catch (IOException e) {
Dialogs.showError(Bundle.CTL_ExportTransectPixelsAction_DialogTitle(),
ERR_MSG_BASE + "Failed to create file '" + file + "':\n" + e.getMessage());
return; // Error
}
out = new PrintWriter(new BufferedWriter(fileWriter, initialBufferSize));
clipboardText = null;
} else {
return; // Cancel
}
final SwingWorker<Exception, Object> swingWorker = new SwingWorker<Exception, Object>() {
@Override
protected Exception doInBackground() throws Exception {
Exception returnValue = null;
ProgressMonitor pm = new DialogProgressMonitor(SnapApp.getDefault().getMainFrame(), Bundle.CTL_ExportTransectPixelsAction_DialogTitle(),
Dialog.ModalityType.APPLICATION_MODAL);
try {
final boolean mustCreateHeader = createHeaderBox.isSelected();
final boolean mustExportWavelengthsAndSF = exportWavelengthsAndSFBox.isSelected();
final boolean mustExportTiePoints = exportTiePointsBox.isSelected();
TransectExporter exporter = new TransectExporter(mustCreateHeader, mustExportWavelengthsAndSF, mustExportTiePoints);
boolean success = exporter.exportTransectPixels(out, raster.getProduct(),
transectProfileData,
numTransectPixels,
pm);
if (success && clipboardText != null) {
SystemUtils.copyToClipboard(clipboardText.toString());
clipboardText.setLength(0);
}
} catch (Exception e) {
returnValue = e;
} finally {
out.close();
}
return returnValue;
}
@Override
public void done() {
// clear status bar
SnapApp.getDefault().setStatusBarMessage("");
// show default-cursor
UIUtils.setRootFrameDefaultCursor(SnapApp.getDefault().getMainFrame());
// On error, show error message
Exception exception;
try {
exception = get();
} catch (Exception e) {
exception = e;
}
if (exception != null) {
Dialogs.showError(Bundle.CTL_ExportTransectPixelsAction_DialogTitle(),
ERR_MSG_BASE + exception.getMessage());
}
}
};
// show wait-cursor
UIUtils.setRootFrameWaitCursor(SnapApp.getDefault().getMainFrame());
// show message in status bar
SnapApp.getDefault().setStatusBarMessage("Exporting transect pixels...");
// Start separate worker thread.
swingWorker.execute();
}
private FigureSelection getCurrentFigureSelection() {
return result.allInstances().stream().findFirst().orElse(null);
}
private void updateEnableState(FigureSelection figureSelection) {
setEnabled(figureSelection != null);
}
private static String createDefaultFileName(final RasterDataNode raster) {
return FileUtils.getFilenameWithoutExtension(raster.getProduct().getName()) + "_TRANSECT.txt";
}
private static String getWindowTitle() {
return SnapApp.getDefault().getInstanceName() + " - " + Bundle.CTL_ExportTransectPixelsAction_DialogTitle();
}
/**
* Opens a modal file chooser dialog that prompts the user to select the output file name.
*
* @return the selected file, {@code null} means "Cancel"
*/
private static File promptForFile(String defaultFileName) {
final SnapFileFilter fileFilter = new SnapFileFilter("TXT", "txt", "Text");
return Dialogs.requestFileForSave(Bundle.CTL_ExportTransectPixelsAction_DialogTitle(),
false,
fileFilter,
".txt",
defaultFileName,
null,
"exportTransectPixels.lastDir");
}
private static int getNumTransectPixels(final Product product,
final TransectProfileData transectProfileData) {
final Point2D[] pixelPositions = transectProfileData.getPixelPositions();
int numTransectPixels = 0;
for (Point2D pixelPosition : pixelPositions) {
int x = (int) Math.floor(pixelPosition.getX());
int y = (int) Math.floor(pixelPosition.getY());
if (x >= 0 && x < product.getSceneRasterWidth()
&& y >= 0 && y < product.getSceneRasterHeight()) {
numTransectPixels++;
}
}
return numTransectPixels;
}
static class TransectExporter {
private final boolean mustCreateHeader;
private final boolean mustExportWavelengthsAndSF;
private final boolean mustExportTiePoints;
TransectExporter(boolean mustCreateHeader, boolean mustExportWavelengthsAndSF, boolean mustExportTiePoints) {
this.mustCreateHeader = mustCreateHeader;
this.mustExportWavelengthsAndSF = mustExportWavelengthsAndSF;
this.mustExportTiePoints = mustExportTiePoints;
}
/**
* Writes all pixel values of the given product within the given ROI to the specified out.
*
* @param out the data output writer
* @param product the product providing the pixel values
* @return {@code true} for success, {@code false} if export has been terminated (by user)
*/
private boolean exportTransectPixels(final PrintWriter out,
final Product product,
final TransectProfileData transectProfileData,
final int numTransectPixels,
ProgressMonitor pm) {
final Band[] bands = product.getBands();
final TiePointGrid[] tiePointGrids = product.getTiePointGrids();
final GeoCoding geoCoding = product.getSceneGeoCoding();
if (mustCreateHeader) {
writeFileHeader(out, bands);
}
writeTableHeader(out, geoCoding, bands, mustExportTiePoints, tiePointGrids, mustExportWavelengthsAndSF);
final Point2D[] pixelPositions = transectProfileData.getPixelPositions();
pm.beginTask("Writing pixel data...", numTransectPixels);
try {
for (Point2D pixelPosition : pixelPositions) {
int x = (int) Math.floor(pixelPosition.getX());
int y = (int) Math.floor(pixelPosition.getY());
if (x >= 0 && x < product.getSceneRasterWidth()
&& y >= 0 && y < product.getSceneRasterHeight()) {
writeDataLine(out, geoCoding, bands, mustExportTiePoints, tiePointGrids, x, y);
pm.worked(1);
if (pm.isCanceled()) {
return false;
}
}
}
} finally {
pm.done();
}
return true;
}
private void writeFileHeader(PrintWriter out, Band[] bands) {
ProductData.UTC utc = ProductData.UTC.create(new Date(), 0);
out.printf("# Exported transect on %s%n", utc.format());
if (bands.length >= 0) {
Product product = bands[0].getProduct();
out.printf("# Product name: %s%n", product.getName());
if (product.getFileLocation() != null) {
out.printf("# Product file location: %s%n", product.getFileLocation().getAbsolutePath());
}
}
out.println();
}
private void writeTableHeader(final PrintWriter out,
final GeoCoding geoCoding,
final Band[] bands,
boolean mustExportTiePoints,
TiePointGrid[] tiePointGrids,
boolean mustExportWavelengthsAndSF) {
if (mustExportWavelengthsAndSF) {
float[] wavelengthArray = new float[bands.length];
for (int i = 0; i < bands.length; i++) {
wavelengthArray[i] = bands[i].getSpectralWavelength();
}
out.printf("# Wavelength:\t \t \t \t%s\n", StringUtils.arrayToString(wavelengthArray, "\t"));
float[] solarFluxArray = new float[bands.length];
for (int i = 0; i < bands.length; i++) {
solarFluxArray[i] = bands[i].getSolarFlux();
}
out.printf("# Solar flux:\t \t \t \t%s%n", StringUtils.arrayToString(solarFluxArray, "\t"));
}
out.print("Pixel-X");
out.print("\t");
out.print("Pixel-Y");
if (geoCoding != null) {
out.print("\t");
out.print("Longitude");
out.print("\t");
out.print("Latitude");
}
for (final Band band : bands) {
out.print("\t");
out.print(band.getName());
}
if (mustExportTiePoints) {
for (final TiePointGrid grid : tiePointGrids) {
out.print("\t");
out.print(grid.getName());
}
}
out.print("\n");
}
/**
* Writes a data line of the dataset to be exported for the given pixel position.
*
* @param out the data output writer
* @param geoCoding the product's geo-coding
* @param bands the array of bands that provide pixel values
* @param mustExportTiePoints if tie-points shall be exported
* @param tiePointGrids the array of tie-points that provide pixel values
* @param x the current pixel's X coordinate
* @param y the current pixel's Y coordinate
*/
private void writeDataLine(final PrintWriter out,
final GeoCoding geoCoding,
final Band[] bands,
boolean mustExportTiePoints,
TiePointGrid[] tiePointGrids,
int x, int y) {
final PixelPos pixelPos = new PixelPos(x + 0.5f, y + 0.5f);
out.print(String.valueOf(pixelPos.x));
out.print("\t");
out.print(String.valueOf(pixelPos.y));
if (geoCoding != null) {
out.print("\t");
final GeoPos geoPos = geoCoding.getGeoPos(pixelPos, null);
out.print(String.valueOf(geoPos.lon));
out.print("\t");
out.print(String.valueOf(geoPos.lat));
}
for (final Band band : bands) {
out.print("\t");
final String pixelString = band.getPixelString(x, y);
out.print(pixelString);
}
if (mustExportTiePoints) {
for (final TiePointGrid grid : tiePointGrids) {
out.print("\t");
out.print(grid.getPixelString(x, y));
}
}
out.print("\n");
}
}
}