/* * 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.mask; import com.bc.ceres.core.ProgressMonitor; import com.bc.ceres.glevel.MultiLevelImage; import com.bc.ceres.swing.progress.DialogProgressMonitor; 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.ProductNodeGroup; import org.esa.snap.core.datamodel.RasterDataNode; import org.esa.snap.core.util.AreaCalculator; import org.esa.snap.core.util.Debug; import org.esa.snap.core.util.math.MathUtils; import org.esa.snap.rcp.SnapApp; import org.esa.snap.rcp.util.Dialogs; import org.esa.snap.ui.AbstractDialog; import org.esa.snap.ui.GridBagUtils; import org.esa.snap.ui.ModalDialog; import org.esa.snap.ui.product.ProductSceneView; import org.openide.awt.ActionID; import org.openide.awt.ActionReference; 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.JComboBox; import javax.swing.JLabel; import javax.swing.JPanel; import javax.swing.JTextField; import javax.swing.SwingWorker; import java.awt.BorderLayout; import java.awt.Dialog; import java.awt.GridBagConstraints; import java.awt.Rectangle; import java.awt.event.ActionEvent; import java.awt.image.Raster; import java.awt.image.RenderedImage; import java.text.MessageFormat; import java.util.ArrayList; import java.util.List; import java.util.concurrent.ExecutionException; @ActionID(category = "Tools", id = "ComputeMaskAreaAction") @ActionRegistration( displayName = "#CTL_ComputeMaskAreaAction_MenuText", popupText = "#CTL_ComputeMaskAreaAction_ShortDescription", lazy = false ) @ActionReference(path = "Menu/Raster/Masks", position = 300) @NbBundle.Messages({ "CTL_ComputeMaskAreaAction_MenuText=Mask Area...", "CTL_ComputeMaskAreaAction_DialogTitle=Compute Mask Area", "CTL_ComputeMaskAreaAction_ShortDescription=Displays information about the spatial area of the mask." }) public class ComputeMaskAreaAction extends AbstractAction implements LookupListener, ContextAwareAction, HelpCtx.Provider { private static final String HELP_ID = "computeMaskArea"; private final Lookup lookup; private Lookup.Result<ProductSceneView> result; public ComputeMaskAreaAction() { this(Utilities.actionsGlobalContext()); } public ComputeMaskAreaAction(Lookup lookup) { super(Bundle.CTL_ComputeMaskAreaAction_MenuText()); this.lookup = lookup; result = lookup.lookupResult(ProductSceneView.class); result.addLookupListener(WeakListeners.create(LookupListener.class, this, result)); setEnableState(); } private void setEnableState() { setEnabled(lookup.lookup(ProductSceneView.class) != null); } @Override public void actionPerformed(ActionEvent event) { computeMaskArea(); } private void computeMaskArea() { final String errMsgBase = "Failed to compute Mask area:\n"; // Get selected Snap view showing a product's band final ProductSceneView view = lookup.lookup(ProductSceneView.class); if (view == null) { Dialogs.showError(Bundle.CTL_ComputeMaskAreaAction_DialogTitle(), errMsgBase + "No view."); return; } // Get the current raster data node (band or tie-point grid) RasterDataNode raster = view.getRaster(); assert raster != null; Product product = raster.getProduct(); final ProductNodeGroup<Mask> maskGroup = product.getMaskGroup(); final List<String> maskNameList = new ArrayList<>(); for (int i = 0; i < maskGroup.getNodeCount(); i++) { final Mask mask = maskGroup.get(i); //todo [multisize_products] ask about scenerastertransform, not size if ((raster.getRasterSize().equals(mask.getRasterSize()))) { maskNameList.add(mask.getName()); } } String[] maskNames = maskNameList.toArray(new String[maskNameList.size()]); String maskName; if (maskNames.length == 0) { Dialogs.showInformation(Bundle.CTL_ComputeMaskAreaAction_DialogTitle(), "No compatible mask available", null); return; } else if (maskNames.length == 1) { maskName = maskNames[0]; } else { JPanel selectPanel = new JPanel(); selectPanel.setLayout(new BoxLayout(selectPanel, BoxLayout.X_AXIS)); selectPanel.add(new JLabel("Select Mask: ")); JComboBox<String> maskCombo = new JComboBox<>(maskNames); selectPanel.add(maskCombo); JPanel dialogPanel = selectPanel; if (product.isMultiSize()) { final JPanel wrapperPanel = new JPanel(new BorderLayout(4, 3)); wrapperPanel.add(selectPanel, BorderLayout.CENTER); wrapperPanel.add(new JLabel("<html><i>Product has rasters of different size. <br/>Only compatible masks are shown.</i>"), BorderLayout.SOUTH); dialogPanel = wrapperPanel; } ModalDialog modalDialog = new ModalDialog(SnapApp.getDefault().getMainFrame(), Bundle.CTL_ComputeMaskAreaAction_DialogTitle(), dialogPanel, ModalDialog.ID_OK_CANCEL | ModalDialog.ID_HELP, getHelpCtx().getHelpID()); if (modalDialog.show() == AbstractDialog.ID_OK) { maskName = (String) maskCombo.getSelectedItem(); if (maskName == null) { return; } } else { return; } } final Mask mask = maskGroup.get(maskName); RenderedImage maskImage = mask.getSourceImage(); if (maskImage == null) { Dialogs.showError(Bundle.CTL_ComputeMaskAreaAction_DialogTitle(), errMsgBase + "No Mask image available."); return; } final SwingWorker<MaskAreaStatistics, Object> swingWorker = new MaskAreaSwingWorker(mask, errMsgBase); swingWorker.execute(); } @Override public Action createContextAwareInstance(Lookup actionContext) { return new ComputeMaskAreaAction(actionContext); } @Override public void resultChanged(LookupEvent ev) { setEnableState(); } @Override public HelpCtx getHelpCtx() { return new HelpCtx(HELP_ID); } private static class MaskAreaStatistics { private double earthRadius; private double maskArea; private double pixelAreaMin; private double pixelAreaMax; private int numPixels; private MaskAreaStatistics(double earthRadius) { this.earthRadius = earthRadius; maskArea = 0.0; pixelAreaMax = Double.NEGATIVE_INFINITY; pixelAreaMin = Double.POSITIVE_INFINITY; numPixels = 0; } public double getEarthRadius() { return earthRadius; } public double getMaskArea() { return maskArea; } public void setMaskArea(double maskArea) { this.maskArea = maskArea; } public double getPixelAreaMin() { return pixelAreaMin; } public void setPixelAreaMin(double pixelAreaMin) { this.pixelAreaMin = pixelAreaMin; } public double getPixelAreaMax() { return pixelAreaMax; } public void setPixelAreaMax(double pixelAreaMax) { this.pixelAreaMax = pixelAreaMax; } public int getNumPixels() { return numPixels; } public void setNumPixels(int numPixels) { this.numPixels = numPixels; } } private class MaskAreaSwingWorker extends SwingWorker<MaskAreaStatistics, Object> { private final RasterDataNode mask; private final String errMsgBase; private MaskAreaSwingWorker(RasterDataNode mask, String errMsgBase) { this.mask = mask; this.errMsgBase = errMsgBase; } @Override protected MaskAreaStatistics doInBackground() throws Exception { ProgressMonitor pm = new DialogProgressMonitor(SnapApp.getDefault().getMainFrame(), "Computing Mask area", Dialog.ModalityType.APPLICATION_MODAL); return computeMaskAreaStatistics(pm); } private MaskAreaStatistics computeMaskAreaStatistics(ProgressMonitor pm) { final MultiLevelImage maskImage = mask.getSourceImage(); final int minTileX = maskImage.getMinTileX(); final int minTileY = maskImage.getMinTileY(); final int numXTiles = maskImage.getNumXTiles(); final int numYTiles = maskImage.getNumYTiles(); final int w = mask.getRasterWidth(); final int h = mask.getRasterHeight(); final Rectangle imageRect = new Rectangle(0, 0, w, h); final PixelPos[] pixelPoints = new PixelPos[5]; final GeoPos[] geoPoints = new GeoPos[5]; for (int i = 0; i < geoPoints.length; i++) { pixelPoints[i] = new PixelPos(); geoPoints[i] = new GeoPos(); } GeoCoding geoCoding = mask.getGeoCoding(); AreaCalculator areaCalculator = new AreaCalculator(geoCoding); MaskAreaStatistics areaStatistics = new MaskAreaStatistics(areaCalculator.getEarthRadius() / 1000.0); pm.beginTask("Computing Mask area...", numXTiles * numYTiles); try { for (int tileX = minTileX; tileX < minTileX + numXTiles; ++tileX) { for (int tileY = minTileY; tileY < minTileY + numYTiles; ++tileY) { if (pm.isCanceled()) { break; } 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) { double pixelArea = areaCalculator.calculatePixelSize(x, y) / Math.pow(1000.0, 2); areaStatistics.setPixelAreaMin(Math.min(areaStatistics.getPixelAreaMin(), pixelArea)); areaStatistics.setPixelAreaMax(Math.max(areaStatistics.getPixelAreaMax(), pixelArea)); areaStatistics.setMaskArea(areaStatistics.getMaskArea() + pixelArea); areaStatistics.setNumPixels(areaStatistics.getNumPixels() + 1); } } } } pm.worked(1); } } } finally { pm.done(); } return areaStatistics; } @Override public void done() { try { final MaskAreaStatistics areaStatistics = get(); if (areaStatistics.getNumPixels() == 0) { final String message = MessageFormat.format("{0}Mask is empty.", errMsgBase); Dialogs.showError(Bundle.CTL_ComputeMaskAreaAction_DialogTitle(), message); } else { showResults(areaStatistics); } } catch (ExecutionException | InterruptedException e) { final String message = MessageFormat.format("An internal Error occurred:\n{0}", e.getMessage()); Dialogs.showError(Bundle.CTL_ComputeMaskAreaAction_DialogTitle(), message); Debug.trace(e); } } private void showResults(MaskAreaStatistics areaStatistics) { final double roundFactor = 10000.0; final double maskAreaR = MathUtils.round(areaStatistics.getMaskArea(), roundFactor); final double meanPixelAreaR = MathUtils.round(areaStatistics.getMaskArea() / areaStatistics.getNumPixels(), roundFactor); final double pixelAreaMinR = MathUtils.round(areaStatistics.getPixelAreaMin(), roundFactor); final double pixelAreaMaxR = MathUtils.round(areaStatistics.getPixelAreaMax(), roundFactor); final JPanel content = GridBagUtils.createPanel(); final GridBagConstraints gbc = new GridBagConstraints(); gbc.fill = GridBagConstraints.HORIZONTAL; gbc.anchor = GridBagConstraints.WEST; gbc.insets.right = 4; gbc.gridy = 0; gbc.weightx = 0; gbc.insets.top = 2; addField(content, gbc, "Number of Mask pixels:", String.format("%15d", areaStatistics.getNumPixels()), ""); addField(content, gbc, "Mask area:", String.format("%15.3f", maskAreaR), "km^2"); addField(content, gbc, "Mean pixel area:", String.format("%15.3f", meanPixelAreaR), "km^2"); addField(content, gbc, "Minimum pixel area:", String.format("%15.3f", pixelAreaMinR), "km^2"); addField(content, gbc, "Maximum pixel area:", String.format("%15.3f", pixelAreaMaxR), "km^2"); gbc.insets.top = 8; addField(content, gbc, "Mean earth radius:", String.format("%15.3f", areaStatistics.getEarthRadius()), "km"); final ModalDialog dialog = new ModalDialog(SnapApp.getDefault().getMainFrame(), Bundle.CTL_ComputeMaskAreaAction_DialogTitle() + " - " + mask.getDisplayName(), content, ModalDialog.ID_OK | ModalDialog.ID_HELP, getHelpCtx().getHelpID()); dialog.show(); } private void addField(final JPanel content, final GridBagConstraints gbc, final String text, final String value, final String unit) { content.add(new JLabel(text), gbc); gbc.weightx = 1; content.add(createTextField(value), gbc); gbc.weightx = 0; content.add(new JLabel(unit), gbc); gbc.gridy++; } private JTextField createTextField(final String value) { JTextField field = new JTextField(value); field.setEditable(false); field.setHorizontalAlignment(JTextField.RIGHT); return field; } } }