/* * 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.rcp.actions.file.export; import com.bc.ceres.core.ProgressMonitor; import com.bc.ceres.swing.progress.ProgressMonitorSwingWorker; 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.Mask; import org.esa.snap.core.datamodel.PixelPos; import org.esa.snap.core.datamodel.Product; import org.esa.snap.core.datamodel.TiePointGrid; 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.AbstractDialog; import org.esa.snap.ui.ModalDialog; 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.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.BoxLayout; import javax.swing.JCheckBox; import javax.swing.JComboBox; import javax.swing.JLabel; import javax.swing.JPanel; import java.awt.Rectangle; import java.awt.event.ActionEvent; import java.awt.image.Raster; import java.awt.image.RenderedImage; 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.text.SimpleDateFormat; import java.util.GregorianCalendar; @ActionID( category = "File", id = "org.esa.snap.rcp.actions.file.export.ExportMaskPixelsAction" ) @ActionRegistration( displayName = "#CTL_ExportMaskPixelsAction_MenuText", popupText = "#CTL_ExportMaskPixelsAction_PopupText", lazy = false ) @ActionReferences({ @ActionReference(path = "Menu/File/Export/Other", position = 50), @ActionReference(path = "Menu/Raster/Export", position = 100), @ActionReference(path = "Context/ProductSceneView", position = 50) }) @NbBundle.Messages({ "CTL_ExportMaskPixelsAction_MenuText=Mask Pixels", "CTL_ExportMaskPixelsAction_PopupText=Export Mask Pixels", "CTL_ExportMaskPixelsAction_DialogTitle=Export Mask Pixels", "CTL_ExportMaskPixelsAction_ShortDescription=Export Mask Pixels." }) public class ExportMaskPixelsAction extends AbstractAction implements ContextAwareAction, LookupListener, HelpCtx.Provider { private static final String HELP_ID = "exportMaskPixels"; private static final String ERR_MSG_BASE = "Mask pixels cannot be exported:\n"; private final Lookup.Result<ProductSceneView> result; public ExportMaskPixelsAction() { this(Utilities.actionsGlobalContext()); } public ExportMaskPixelsAction(Lookup lkp) { super(Bundle.CTL_ExportMaskPixelsAction_MenuText()); putValue("popupText", Bundle.CTL_ExportMaskPixelsAction_PopupText()); result = lkp.lookupResult(ProductSceneView.class); result.addLookupListener(WeakListeners.create(LookupListener.class, this, result)); setEnabled(false); } /** * Invoked when a command action is performed. * * @param event the command event */ @Override public void actionPerformed(ActionEvent event) { ProductSceneView sceneView = SnapApp.getDefault().getSelectedProductSceneView(); if (sceneView != null) { Product product = sceneView.getProduct(); if (product.isMultiSize()) { final Product resampledProduct = MultiSizeIssue.maybeResample(product); if (resampledProduct != null) { product = resampledProduct; } else { return; } } exportMaskPixels(product); } } @Override public Action createContextAwareInstance(Lookup lkp) { return new ExportMaskPixelsAction(lkp); } @Override public void resultChanged(LookupEvent le) { ProductSceneView sceneView = SnapApp.getDefault().getSelectedProductSceneView(); boolean enabled = false; if (sceneView != null) { Product product = sceneView.getProduct(); enabled = product.getMaskGroup().getNodeCount() > 0; } setEnabled(enabled); } ///////////////////////////////////////////////////////////////////////// // Private implementations for the "export Mask Pixels" command ///////////////////////////////////////////////////////////////////////// /** * Performs the actual "export Mask Pixels" command. * @param product */ private void exportMaskPixels(Product product) { String[] maskNames = product.getMaskGroup().getNodeNames(); final String maskName; if (maskNames.length == 1) { maskName = maskNames[0]; } else { JPanel panel = new JPanel(); BoxLayout boxLayout = new BoxLayout(panel, BoxLayout.X_AXIS); panel.setLayout(boxLayout); panel.add(new JLabel("Select Mask: ")); JComboBox<String> maskCombo = new JComboBox<>(maskNames); panel.add(maskCombo); ModalDialog modalDialog = new ModalDialog(SnapApp.getDefault().getMainFrame(), Bundle.CTL_ExportMaskPixelsAction_DialogTitle(), panel, ModalDialog.ID_OK_CANCEL | ModalDialog.ID_HELP, getHelpCtx().getHelpID()); if (modalDialog.show() == AbstractDialog.ID_OK) { maskName = (String) maskCombo.getSelectedItem(); } else { return; } } Mask mask = product.getMaskGroup().get(maskName); final RenderedImage maskImage = mask.getSourceImage(); if (maskImage == null) { Dialogs.showError(Bundle.CTL_ExportMaskPixelsAction_DialogTitle(), ERR_MSG_BASE + "No Mask image available."); return; } // Compute total number of Mask pixels final long numMaskPixels = getNumMaskPixels(maskImage, product.getSceneRasterWidth(), product.getSceneRasterHeight()); String numPixelsText; if (numMaskPixels == 1) { numPixelsText = "One Mask pixel will be exported.\n"; } else { numPixelsText = numMaskPixels + " Mask 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 }, getHelpCtx().getHelpID()); final boolean mustCreateHeader = createHeaderBox.isSelected(); final boolean mustExportTiePoints = exportTiePointsBox.isSelected(); final boolean mustExportWavelengthsAndSF = exportWavelengthsAndSFBox.isSelected(); // 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(product, maskName)); if (file == null) { return; // Cancel } final FileWriter fileWriter; try { fileWriter = new FileWriter(file); } catch (IOException e) { Dialogs.showError(Bundle.CTL_ExportMaskPixelsAction_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 ProgressMonitorSwingWorker<Exception, Object> swingWorker = new ProgressMonitorSwingWorker<Exception, Object>( SnapApp.getDefault().getMainFrame(), Bundle.CTL_ExportMaskPixelsAction_DialogTitle()) { @Override protected Exception doInBackground(ProgressMonitor pm) throws Exception { Exception returnValue = null; try { boolean success = exportMaskPixels(out, product, maskImage, maskName, mustCreateHeader, mustExportTiePoints, mustExportWavelengthsAndSF, 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_ExportMaskPixelsAction_DialogTitle(), ERR_MSG_BASE + exception.getMessage()); } } }; // show wait-cursor UIUtils.setRootFrameWaitCursor(SnapApp.getDefault().getMainFrame()); // show message in status bar SnapApp.getDefault().setStatusBarMessage("Exporting Mask pixels..."); // Start separate worker thread. swingWorker.execute(); } private static String createDefaultFileName(final Product raster, String maskName) { String productName = FileUtils.getFilenameWithoutExtension(raster.getProduct().getName()); return productName + "_" + maskName + "_Mask.txt"; } private static String getWindowTitle() { return SnapApp.getDefault().getInstanceName() + " - " + Bundle.CTL_ExportMaskPixelsAction_DialogTitle(); } /* * Opens a modal file chooser dialog that prompts the user to select the output file name. * * @param visatApp the VISAT application * @return the selected file, <code>null</code> means "Cancel" */ private static File promptForFile(String defaultFileName) { final SnapFileFilter fileFilter = new SnapFileFilter("TXT", "txt", "Text"); return Dialogs.requestFileForSave(Bundle.CTL_ExportMaskPixelsAction_DialogTitle(), false, fileFilter, ".txt", defaultFileName, null, "exportMaskPixels.lastDir"); } /* * Writes all pixel values of the given product within the given Mask to the specified out. * * @param out the data output writer * @param product the product providing the pixel values * @param maskImage the mask image for the Mask * @return <code>true</code> for success, <code>false</code> if export has been terminated (by user) */ private static boolean exportMaskPixels(final PrintWriter out, final Product product, final RenderedImage maskImage, String maskName, boolean mustCreateHeader, boolean mustExportTiePoints, boolean mustExportWavelengthsAndSF, ProgressMonitor pm) throws IOException { final Band[] bands = product.getBands(); final TiePointGrid[] tiePointGrids = product.getTiePointGrids(); final GeoCoding geoCoding = product.getSceneGeoCoding(); final int minTileX = maskImage.getMinTileX(); final int minTileY = maskImage.getMinTileY(); final int numXTiles = maskImage.getNumXTiles(); final int numYTiles = maskImage.getNumYTiles(); final int w = product.getSceneRasterWidth(); final int h = product.getSceneRasterHeight(); final Rectangle imageRect = new Rectangle(0, 0, w, h); pm.beginTask("Writing pixel data...", numXTiles * numYTiles + 2); try { if (mustCreateHeader) { createHeader(out, product, maskName, mustExportWavelengthsAndSF); } pm.worked(1); writeColumnNames(out, geoCoding, bands, mustExportTiePoints, tiePointGrids); pm.worked(1); for (int tileX = minTileX; tileX < minTileX + numXTiles; ++tileX) { for (int tileY = minTileY; tileY < minTileY + numYTiles; ++tileY) { if (pm.isCanceled()) { return false; } final Rectangle tileRectangle = new Rectangle(maskImage.getTileGridXOffset() + tileX * maskImage.getTileWidth(), maskImage.getTileGridYOffset() + tileY * maskImage.getTileHeight(), maskImage.getTileWidth(), maskImage.getTileHeight()); final Rectangle r = imageRect.intersection(tileRectangle); if (!r.isEmpty()) { Raster maskTile = maskImage.getTile(tileX, tileY); for (int y = r.y; y < r.y + r.height; y++) { for (int x = r.x; x < r.x + r.width; x++) { if (maskTile.getSample(x, y, 0) != 0) { writeDataLine(out, geoCoding, bands, mustExportTiePoints, tiePointGrids, x, y); } } } } pm.worked(1); } } } finally { pm.done(); } return true; } private static void createHeader(PrintWriter out, Product product, String maskName, boolean mustExportWavelengthsAndSF) { out.write("# Exported mask '" + maskName + "' on " + new SimpleDateFormat("dd-MMM-yyyy HH:mm:ss.mmmmmm").format(new GregorianCalendar().getTime()) + "\n"); out.write("# Product name: " + product.getName() + "\n"); if (product.getFileLocation() != null) { out.write("# Product file location: " + product.getFileLocation() + "\n"); } out.write("\n"); if (mustExportWavelengthsAndSF) { out.write("# Wavelength:"); out.write("\t\t\t"); // account for pixel-x, pixel-y, lon, lat columns for (final Band band : product.getBands()) { out.print("\t"); out.print("" + band.getSpectralWavelength()); } out.print("\n"); out.write("# Solar flux:"); out.write("\t\t\t"); // account for pixel-x, pixel-y, lon, lat columns for (final Band band : product.getBands()) { out.print("\t"); out.print("" + band.getSolarFlux()); } out.print("\n"); } } /* * Writes the header line of the dataset to be exported. * * @param out the data output writer * @param geoCoding the product's geo-coding * @param bands the array of bands to be considered * @param mustExportTiePointGrids if tie-point grids shall be considered * @param tiePointGrids the array of tie-point grids to be considered */ private static void writeColumnNames(final PrintWriter out, final GeoCoding geoCoding, final Band[] bands, boolean mustExportTiePointGrids, TiePointGrid[] tiePointGrids) { 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 (mustExportTiePointGrids) { 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 mustExportTiePointGrids if tie-point grids shall be exported * @param tiePointGrids the array of tie-point grids that provide pixel values * @param x the current pixel's X coordinate * @param y the current pixel's Y coordinate */ private static void writeDataLine(final PrintWriter out, final GeoCoding geoCoding, final Band[] bands, boolean mustExportTiePoints, TiePointGrid[] tiePointGrids, int x, int y) throws IOException { 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) { final GeoPos geoPos = geoCoding.getGeoPos(pixelPos, null); out.print("\t"); out.print(String.valueOf(geoPos.lon)); out.print("\t"); out.print(String.valueOf(geoPos.lat)); } final int[] intPixel = new int[1]; final float[] floatPixel = new float[1]; for (final Band band : bands) { out.print("\t"); if (band.isPixelValid(x, y)) { if (band.isFloatingPointType()) { band.readPixels(x, y, 1, 1, floatPixel, ProgressMonitor.NULL); out.print(floatPixel[0]); } else { band.readPixels(x, y, 1, 1, intPixel, ProgressMonitor.NULL); out.print(intPixel[0]); } } else { out.print("NaN"); } } if (mustExportTiePoints) { for (final TiePointGrid grid : tiePointGrids) { grid.readPixels(x, y, 1, 1, floatPixel, ProgressMonitor.NULL); out.print("\t"); out.print(floatPixel[0]); } } out.print("\n"); } /* * Computes the total number of pixels within the specified Mask. * * @param raster the raster data node * @param maskImage the rendered image masking out the Mask * @return the total number of pixels in the Mask */ private static long getNumMaskPixels(final RenderedImage maskImage, int sceneRasterWidth, int sceneRasterHeight) { final int minTileX = maskImage.getMinTileX(); final int minTileY = maskImage.getMinTileY(); final int numXTiles = maskImage.getNumXTiles(); final int numYTiles = maskImage.getNumYTiles(); final Rectangle imageRect = new Rectangle(0, 0, sceneRasterWidth, sceneRasterHeight); long numMaskPixels = 0; for (int tileX = minTileX; tileX < minTileX + numXTiles; ++tileX) { for (int tileY = minTileY; tileY < minTileY + numYTiles; ++tileY) { final Rectangle tileRectangle = new Rectangle(maskImage.getTileGridXOffset() + tileX * maskImage.getTileWidth(), maskImage.getTileGridYOffset() + tileY * maskImage.getTileHeight(), maskImage.getTileWidth(), maskImage.getTileHeight()); final Rectangle r = imageRect.intersection(tileRectangle); if (!r.isEmpty()) { Raster maskTile = maskImage.getTile(tileX, tileY); for (int y = r.y; y < r.y + r.height; y++) { for (int x = r.x; x < r.x + r.width; x++) { if (maskTile.getSample(x, y, 0) != 0) { numMaskPixels++; } } } } } } return numMaskPixels; } @Override public HelpCtx getHelpCtx() { return new HelpCtx(HELP_ID); } }