/* * 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.binding.Property; import com.bc.ceres.binding.PropertyContainer; import com.bc.ceres.binding.PropertyDescriptor; import com.bc.ceres.binding.accessors.DefaultPropertyAccessor; import com.bc.ceres.binding.converters.IntegerConverter; import com.bc.ceres.glayer.support.ImageLayer; import com.bc.ceres.grender.Viewport; import com.bc.ceres.grender.support.BufferedImageRendering; import com.bc.ceres.grender.support.DefaultViewport; import com.bc.ceres.swing.binding.BindingContext; import com.bc.ceres.swing.binding.PropertyPane; import org.esa.snap.core.util.io.SnapFileFilter; 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.SnapFileChooser; 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.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.Action; import javax.swing.BorderFactory; import javax.swing.BoxLayout; import javax.swing.ButtonGroup; import javax.swing.ButtonModel; import javax.swing.JComponent; import javax.swing.JPanel; import javax.swing.JRadioButton; import java.awt.Dimension; import java.awt.Graphics2D; import java.awt.GridLayout; import java.awt.Rectangle; import java.awt.event.ActionEvent; import java.awt.geom.AffineTransform; import java.awt.geom.Point2D; import java.awt.geom.Rectangle2D; import java.awt.image.BufferedImage; import java.awt.image.RenderedImage; /** * Action for exporting scene views as images. * * @author Marco Peters * @author Ralf Quast */ @ActionID(category = "File", id = "org.esa.snap.rcp.actions.file.export.ExportImageAction") @ActionRegistration( displayName = "#CTL_ExportImageAction_MenuText", popupText = "#CTL_ExportImageAction_PopupText", lazy = false ) @ActionReferences({ @ActionReference(path = "Menu/File/Export/Other", position = 80, separatorAfter = 200), @ActionReference(path = "Context/ProductSceneView", position = 70) }) @NbBundle.Messages({ "CTL_ExportImageAction_MenuText=View as Image", "CTL_ExportImageAction_PopupText=Export View as Image", "CTL_ExportImageAction_ShortDescription=Export the current view as an image." }) public class ExportImageAction extends AbstractExportImageAction { private final static String[][] SCENE_IMAGE_FORMAT_DESCRIPTIONS = { BMP_FORMAT_DESCRIPTION, PNG_FORMAT_DESCRIPTION, JPEG_FORMAT_DESCRIPTION, TIFF_FORMAT_DESCRIPTION, GEOTIFF_FORMAT_DESCRIPTION, }; private static final String HELP_ID = "exportImageFile"; private static final String AC_VIEW_RES = "viewRes"; private static final String AC_FULL_RES = "fullRes"; private static final String AC_USER_RES = "userRes"; private static final String AC_FULL_REGION = "fullRegion"; private static final String AC_VIEW_REGION = "viewRegion"; private static final String PSEUDO_AC_INIT = "INIT"; private SnapFileFilter[] sceneImageFileFilters; private SizeComponent sizeComponent; @SuppressWarnings("FieldCanBeLocal") private Lookup.Result<ProductSceneView> result; private ButtonGroup buttonGroupResolution; private ButtonGroup buttonGroupRegion; private JRadioButton buttonVisibleRegion; private JRadioButton buttonViewResolution; private JRadioButton buttonFullResolution; private JRadioButton buttonUserResolution; private JRadioButton buttonFullRegion; public ExportImageAction() { this(Utilities.actionsGlobalContext()); } public ExportImageAction(Lookup lookup) { super(Bundle.CTL_ExportImageAction_MenuText(), HELP_ID); putValue("popupText", Bundle.CTL_ExportImageAction_PopupText()); sceneImageFileFilters = new SnapFileFilter[SCENE_IMAGE_FORMAT_DESCRIPTIONS.length]; for (int i = 0; i < SCENE_IMAGE_FORMAT_DESCRIPTIONS.length; i++) { sceneImageFileFilters[i] = createFileFilter(SCENE_IMAGE_FORMAT_DESCRIPTIONS[i]); } result = lookup.lookupResult(ProductSceneView.class); result.addLookupListener(WeakListeners.create(LookupListener.class, this, result)); setEnabled(false); } @Override public void actionPerformed(ActionEvent event) { exportImage(sceneImageFileFilters); } @Override public Action createContextAwareInstance(Lookup lookup) { return new ExportImageAction(lookup); } @Override public void resultChanged(LookupEvent lookupEvent) { setEnabled(SnapApp.getDefault().getSelectedProductSceneView() != null); } protected void configureFileChooser(final SnapFileChooser fileChooser, final ProductSceneView view, String imageBaseName) { fileChooser.setDialogTitle(SnapApp.getDefault().getInstanceName() + " - " + "Export Image"); /*I18N*/ if (view.isRGB()) { fileChooser.setCurrentFilename(imageBaseName + "_RGB"); } else { fileChooser.setCurrentFilename(imageBaseName + "_" + view.getRaster().getName()); } final JPanel regionPanel = new JPanel(new GridLayout(2, 1)); regionPanel.setBorder(BorderFactory.createTitledBorder("Image Region")); buttonFullRegion = new JRadioButton("Full scene"); buttonFullRegion.setActionCommand(AC_FULL_REGION); buttonVisibleRegion = new JRadioButton("View region"); buttonVisibleRegion.setActionCommand(AC_VIEW_REGION); regionPanel.add(buttonVisibleRegion); regionPanel.add(buttonFullRegion); buttonGroupRegion = new ButtonGroup(); buttonGroupRegion.add(buttonVisibleRegion); buttonGroupRegion.add(buttonFullRegion); final JPanel resolutionPanel = new JPanel(new GridLayout(3, 1)); resolutionPanel.setBorder(BorderFactory.createTitledBorder("Image Resolution")); buttonViewResolution = new JRadioButton("View resolution"); buttonViewResolution.setActionCommand(AC_VIEW_RES); buttonFullResolution = new JRadioButton("Full resolution"); buttonFullResolution.setActionCommand(AC_FULL_RES); buttonUserResolution = new JRadioButton("User resolution"); buttonUserResolution.setActionCommand(AC_USER_RES); resolutionPanel.add(buttonViewResolution); resolutionPanel.add(buttonFullResolution); resolutionPanel.add(buttonUserResolution); buttonGroupResolution = new ButtonGroup(); buttonGroupResolution.add(buttonViewResolution); buttonGroupResolution.add(buttonFullResolution); buttonGroupResolution.add(buttonUserResolution); sizeComponent = new SizeComponent(view); JComponent sizePanel = sizeComponent.createComponent(); sizePanel.setBorder(BorderFactory.createTitledBorder("Image Dimension")); /*I18N*/ final JPanel accessory = new JPanel(); accessory.setLayout(new BoxLayout(accessory, BoxLayout.Y_AXIS)); accessory.add(regionPanel); accessory.add(resolutionPanel); accessory.add(sizePanel); fileChooser.setAccessory(accessory); buttonVisibleRegion.addActionListener(e -> updateComponents(e.getActionCommand())); buttonFullRegion.addActionListener(e -> updateComponents(e.getActionCommand())); buttonViewResolution.addActionListener(e -> updateComponents(e.getActionCommand())); buttonFullResolution.addActionListener(e -> updateComponents(e.getActionCommand())); buttonUserResolution.addActionListener(e -> updateComponents(e.getActionCommand())); updateComponents(PSEUDO_AC_INIT); } private void updateComponents(String actionCommand) { updateEnableState(actionCommand); sizeComponent.updateDimensions(); } private void updateEnableState(String actionCommand) { switch (actionCommand) { case AC_FULL_REGION: buttonViewResolution.setEnabled(false); if (buttonViewResolution.isSelected()) { buttonFullResolution.setSelected(true); } break; case AC_VIEW_REGION: buttonViewResolution.setEnabled(true); break; case PSEUDO_AC_INIT: buttonVisibleRegion.setSelected(true); buttonViewResolution.setSelected(true); } switch (actionCommand) { case AC_FULL_RES: sizeComponent.setEnabled(false); break; case AC_VIEW_RES: sizeComponent.setEnabled(false); break; case AC_USER_RES: sizeComponent.setEnabled(true); break; case PSEUDO_AC_INIT: sizeComponent.setEnabled(false); } } protected RenderedImage createImage(String imageFormat, ProductSceneView view) { final boolean useAlpha = !BMP_FORMAT_DESCRIPTION[0].equals(imageFormat) && !JPEG_FORMAT_DESCRIPTION[0].equals(imageFormat); final boolean entireImage = isEntireImageSelected(); return createImage(view, entireImage, sizeComponent.getDimension(), useAlpha, GEOTIFF_FORMAT_DESCRIPTION[0].equals(imageFormat)); } static RenderedImage createImage(ProductSceneView view, boolean fullScene, Dimension dimension, boolean alphaChannel, boolean geoReferenced) { final int imageType = alphaChannel ? BufferedImage.TYPE_4BYTE_ABGR : BufferedImage.TYPE_3BYTE_BGR; final BufferedImage bufferedImage = new BufferedImage(dimension.width, dimension.height, imageType); final BufferedImageRendering imageRendering = createRendering(view, fullScene, geoReferenced, bufferedImage); if (!alphaChannel) { final Graphics2D graphics = imageRendering.getGraphics(); graphics.setColor(view.getLayerCanvas().getBackground()); graphics.fillRect(0, 0, dimension.width, dimension.height); } view.getRootLayer().render(imageRendering); return bufferedImage; } private static BufferedImageRendering createRendering(ProductSceneView view, boolean fullScene, boolean geoReferenced, BufferedImage bufferedImage) { final Viewport vp1 = view.getLayerCanvas().getViewport(); final Viewport vp2 = new DefaultViewport(new Rectangle(bufferedImage.getWidth(), bufferedImage.getHeight()), vp1.isModelYAxisDown()); if (fullScene) { vp2.zoom(view.getBaseImageLayer().getModelBounds()); } else { setTransform(vp1, vp2); } final BufferedImageRendering imageRendering = new BufferedImageRendering(bufferedImage, vp2); if (geoReferenced) { // because image to model transform is stored with the exported image we have to invert // image to view transformation final AffineTransform m2iTransform = view.getBaseImageLayer().getModelToImageTransform(0); final AffineTransform v2mTransform = vp2.getViewToModelTransform(); v2mTransform.preConcatenate(m2iTransform); final AffineTransform v2iTransform = new AffineTransform(v2mTransform); final Graphics2D graphics2D = imageRendering.getGraphics(); v2iTransform.concatenate(graphics2D.getTransform()); graphics2D.setTransform(v2iTransform); } return imageRendering; } private static void setTransform(Viewport vp1, Viewport vp2) { vp2.setTransform(vp1); final Rectangle rectangle1 = vp1.getViewBounds(); final Rectangle rectangle2 = vp2.getViewBounds(); final double w1 = rectangle1.getWidth(); final double w2 = rectangle2.getWidth(); final double h1 = rectangle1.getHeight(); final double h2 = rectangle2.getHeight(); final double x1 = rectangle1.getX(); final double y1 = rectangle1.getY(); final double cx = (x1 + w1) / 2.0; final double cy = (y1 + h1) / 2.0; final double magnification; if (w1 > h1) { magnification = w2 / w1; } else { magnification = h2 / h1; } final Point2D modelCenter = vp1.getViewToModelTransform().transform(new Point2D.Double(cx, cy), null); final double zoomFactor = vp1.getZoomFactor() * magnification; if (zoomFactor > 0.0) { vp2.setZoomFactor(zoomFactor, modelCenter.getX(), modelCenter.getY()); } } protected boolean isEntireImageSelected() { ButtonModel selection = buttonGroupRegion.getSelection(); return selection != null && AC_FULL_REGION.equals(selection.getActionCommand()); } private class SizeComponent { private static final String PROPERTY_NAME_HEIGHT = "height"; private static final String PROPERTY_NAME_WIDTH = "width"; private final PropertyContainer propertyContainer; private final ProductSceneView view; private BindingContext bindingContext; SizeComponent(ProductSceneView view) { this.view = view; propertyContainer = new PropertyContainer(); initValueContainer(); bindingContext = new BindingContext(propertyContainer); } private void setEnabled(boolean enabled) { bindingContext.setComponentsEnabled(PROPERTY_NAME_HEIGHT, enabled); bindingContext.setComponentsEnabled(PROPERTY_NAME_WIDTH, enabled); } private void updateDimensions() { final Rectangle2D bounds; ButtonModel selection = buttonGroupResolution.getSelection(); if (selection == null) { return; } String resolutionAC = selection.getActionCommand(); if (isEntireImageSelected()) { final ImageLayer imageLayer = view.getBaseImageLayer(); final Rectangle2D modelBounds = imageLayer.getModelBounds(); Rectangle2D imageBounds = imageLayer.getModelToImageTransform().createTransformedShape(modelBounds).getBounds2D(); final double mScale = modelBounds.getWidth() / modelBounds.getHeight(); final double iScale = imageBounds.getHeight() / imageBounds.getWidth(); double scaleFactorX = mScale * iScale; bounds = new Rectangle2D.Double(0, 0, scaleFactorX * imageBounds.getWidth(), 1 * imageBounds.getHeight()); } else { switch (resolutionAC) { case AC_FULL_RES: bounds = view.getVisibleImageBounds(); break; case AC_VIEW_RES: bounds = view.getLayerCanvas().getViewport().getViewBounds(); break; default: // AC_USER_RES bounds = new Rectangle(getWidth(), getHeight()); break; } } int w = toInteger(bounds.getWidth()); int h = toInteger(bounds.getHeight()); final double freeMemory = getFreeMemory(); final double expectedMemory = getExpectedMemory(w, h); if (freeMemory < expectedMemory) { if (showQuestionDialog() != Dialogs.Answer.YES) { final double scale = Math.sqrt(freeMemory / expectedMemory); final double scaledW = w * scale; final double scaledH = h * scale; w = toInteger(scaledW); h = toInteger(scaledH); } } setWidth(w); setHeight(h); } private int toInteger(double value) { return MathUtils.floorInt(value); } private JComponent createComponent() { PropertyPane propertyPane = new PropertyPane(bindingContext); return propertyPane.createPanel(); } public Dimension getDimension() { return new Dimension(getWidth(), getHeight()); } private void initValueContainer() { final PropertyDescriptor widthDescriptor = new PropertyDescriptor(PROPERTY_NAME_WIDTH, Integer.class); widthDescriptor.setConverter(new IntegerConverter()); propertyContainer.addProperty(new Property(widthDescriptor, new DefaultPropertyAccessor())); final PropertyDescriptor heightDescriptor = new PropertyDescriptor(PROPERTY_NAME_HEIGHT, Integer.class); heightDescriptor.setConverter(new IntegerConverter()); propertyContainer.addProperty(new Property(heightDescriptor, new DefaultPropertyAccessor())); } private Dialogs.Answer showQuestionDialog() { return Dialogs.requestDecision(Bundle.CTL_ExportImageAction_MenuText(), "There may not be enough memory to export the image because\n" + "the image dimension is too large. \n Do you really want to keep the image dimension?", true, null); } private long getFreeMemory() { final long usedMemory = Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory(); return Runtime.getRuntime().maxMemory() - usedMemory; } private long getExpectedMemory(int width, int height) { return width * height * 6L; } private int getWidth() { return (Integer) propertyContainer.getValue(PROPERTY_NAME_WIDTH); } private void setWidth(Object value) { propertyContainer.setValue(PROPERTY_NAME_WIDTH, value); } private int getHeight() { return (Integer) propertyContainer.getValue(PROPERTY_NAME_HEIGHT); } private void setHeight(Object value) { propertyContainer.setValue(PROPERTY_NAME_HEIGHT, value); } } }