/*
* 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 com.sun.media.jai.codec.ImageCodec;
import com.sun.media.jai.codec.ImageEncoder;
import org.esa.snap.core.datamodel.CrsGeoCoding;
import org.esa.snap.core.datamodel.GeoCoding;
import org.esa.snap.core.datamodel.GeoPos;
import org.esa.snap.core.datamodel.ImageLegend;
import org.esa.snap.core.datamodel.MapGeoCoding;
import org.esa.snap.core.datamodel.PixelPos;
import org.esa.snap.core.datamodel.Placemark;
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.dataop.maptransf.IdentityTransformDescriptor;
import org.esa.snap.core.dataop.maptransf.MapTransformDescriptor;
import org.esa.snap.core.util.Debug;
import org.esa.snap.core.util.SystemUtils;
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.runtime.Config;
import org.esa.snap.ui.SnapFileChooser;
import org.esa.snap.ui.product.ProductSceneView;
import org.geotools.referencing.CRS;
import org.geotools.referencing.crs.DefaultGeographicCRS;
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.JFileChooser;
import java.awt.Cursor;
import java.awt.Dimension;
import java.awt.event.ActionEvent;
import java.awt.image.RenderedImage;
import java.io.File;
import java.io.FileOutputStream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
@ActionID(
category = "File",
id = "org.esa.snap.rcp.actions.file.export.ExportKmzFileAction"
)
@ActionRegistration(
displayName = "#CTL_ExportKmzFileAction_MenuText",
popupText = "#CTL_ExportKmzFileAction_PopupText",
lazy = false
)
@ActionReferences({
@ActionReference(path = "Menu/File/Export/Other", position = 80),
@ActionReference(path = "Context/ProductSceneView", position = 60)
})
@NbBundle.Messages({
"CTL_ExportKmzFileAction_MenuText=View as Google Earth KMZ",
"CTL_ExportKmzFileAction_PopupText=Export View as Google Earth KMZ",
"CTL_ExportKmzFileAction_ShortDescription=Export View as Google Earth KMZ."
})
public class ExportKmzFileAction extends AbstractAction implements HelpCtx.Provider, ContextAwareAction, LookupListener {
private static final String OVERLAY_KML = "overlay.kml";
private static final String OVERLAY_PNG = "overlay.png";
private static final String IMAGE_TYPE = "PNG";
private static final String LEGEND_PNG = "legend.png";
private static final String[] KMZ_FORMAT_DESCRIPTION = {"KMZ", "kmz", "KMZ - Google Earth File Format"};
private static final String IMAGE_EXPORT_DIR_PREFERENCES_KEY = "user.image.export.dir";
private static final String HELP_ID = "exportKmzFile";
@SuppressWarnings("FieldCanBeLocal")
private final Lookup.Result<ProductSceneView> result;
public ExportKmzFileAction() {
this(Utilities.actionsGlobalContext());
}
public ExportKmzFileAction(Lookup lookup) {
super(Bundle.CTL_ExportKmzFileAction_MenuText());
putValue("popupText",Bundle.CTL_ExportKmzFileAction_PopupText());
result = lookup.lookupResult(ProductSceneView.class);
result.addLookupListener(WeakListeners.create(LookupListener.class, this, result));
setEnabled(false);
}
@Override
public void actionPerformed(ActionEvent e) {
ProductSceneView view = SnapApp.getDefault().getSelectedProductSceneView();
final GeoCoding geoCoding = view.getProduct().getSceneGeoCoding();
boolean isGeographic = false;
if (geoCoding instanceof MapGeoCoding) {
MapGeoCoding mapGeoCoding = (MapGeoCoding) geoCoding;
MapTransformDescriptor transformDescriptor = mapGeoCoding.getMapInfo()
.getMapProjection().getMapTransform().getDescriptor();
String typeID = transformDescriptor.getTypeID();
if (typeID.equals(IdentityTransformDescriptor.TYPE_ID)) {
isGeographic = true;
}
} else if (geoCoding instanceof CrsGeoCoding) {
isGeographic = CRS.equalsIgnoreMetadata(geoCoding.getMapCRS(), DefaultGeographicCRS.WGS84);
}
if (isGeographic) {
exportImage(view);
} else {
String message = "Product must be in ''Geographic Lat/Lon'' projection.";
Dialogs.showInformation(message, null);
}
}
@Override
public HelpCtx getHelpCtx() {
return new HelpCtx(HELP_ID);
}
@Override
public Action createContextAwareInstance(Lookup lookup) {
return new ExportKmzFileAction(lookup);
}
@Override
public void resultChanged(LookupEvent lookupEvent) {
setEnabled(SnapApp.getDefault().getSelectedProductSceneView() != null);
}
private void exportImage(ProductSceneView sceneView) {
SnapApp snapApp = SnapApp.getDefault();
final String lastDir = Config.instance().load().preferences().get(
IMAGE_EXPORT_DIR_PREFERENCES_KEY,
SystemUtils.getUserHomeDir().getPath());
final File currentDir = new File(lastDir);
final SnapFileChooser fileChooser = new SnapFileChooser();
HelpCtx.setHelpIDString(fileChooser, getHelpCtx().getHelpID());
SnapFileFilter kmzFileFilter = new SnapFileFilter(KMZ_FORMAT_DESCRIPTION[0],
KMZ_FORMAT_DESCRIPTION[1],
KMZ_FORMAT_DESCRIPTION[2]);
fileChooser.setCurrentDirectory(currentDir);
fileChooser.addChoosableFileFilter(kmzFileFilter);
fileChooser.setAcceptAllFileFilterUsed(false);
fileChooser.setDialogTitle(snapApp.getInstanceName() + " - " + "export KMZ");
final String currentFilename = sceneView.isRGB() ? "RGB" : sceneView.getRaster().getName();
fileChooser.setCurrentFilename(currentFilename);
fileChooser.setFileSelectionMode(JFileChooser.FILES_ONLY);
Dimension fileChooserSize = fileChooser.getPreferredSize();
if (fileChooserSize != null) {
fileChooser.setPreferredSize(new Dimension(
fileChooserSize.width + 120, fileChooserSize.height));
} else {
fileChooser.setPreferredSize(new Dimension(512, 256));
}
int result = fileChooser.showSaveDialog(snapApp.getMainFrame());
File file = fileChooser.getSelectedFile();
fileChooser.addPropertyChangeListener(evt -> {
// @todo never comes here, why?
Debug.trace(evt.toString());
});
final File currentDirectory = fileChooser.getCurrentDirectory();
if (currentDirectory != null) {
Config.instance().load().preferences().get(
IMAGE_EXPORT_DIR_PREFERENCES_KEY,
currentDirectory.getPath());
}
if (result != JFileChooser.APPROVE_OPTION) {
return;
}
if (file == null || file.getName().isEmpty()) {
return;
}
if (!Dialogs.requestOverwriteDecision("h", file)) {
return;
}
final SaveKMLSwingWorker worker = new SaveKMLSwingWorker(snapApp, "Save KMZ", sceneView, file);
worker.executeWithBlocking();
}
private static RenderedImage createImageLegend(RasterDataNode raster) {
ImageLegend imageLegend = initImageLegend(raster);
return imageLegend.createImage();
}
private static String formatKML(ProductSceneView view, String imageName) {
final RasterDataNode raster = view.getRaster();
final Product product = raster.getProduct();
final GeoCoding geoCoding = raster.getGeoCoding();
final PixelPos upperLeftPP = new PixelPos(0, 0);
final PixelPos lowerRightPP = new PixelPos(product.getSceneRasterWidth(),
product.getSceneRasterHeight());
final GeoPos upperLeftGP = geoCoding.getGeoPos(upperLeftPP, null);
final GeoPos lowerRightGP = geoCoding.getGeoPos(lowerRightPP, null);
double eastLon = lowerRightGP.getLon();
if (geoCoding.isCrossingMeridianAt180()) {
eastLon += 360;
}
String pinKml = "";
ProductNodeGroup<Placemark> pinGroup = product.getPinGroup();
Placemark[] pins = pinGroup.toArray(new Placemark[pinGroup.getNodeCount()]);
for (Placemark placemark : pins) {
GeoPos geoPos = placemark.getGeoPos();
if (geoPos != null && product.containsPixel(placemark.getPixelPos())) {
pinKml += String.format(
"<Placemark>\n"
+ " <name>%s</name>\n"
+ " <Point>\n"
+ " <coordinates>%f,%f,0</coordinates>\n"
+ " </Point>\n"
+ "</Placemark>\n",
placemark.getLabel(),
geoPos.lon,
geoPos.lat);
}
}
String name;
String description;
String legendKml = "";
if (view.isRGB()) {
name = "RGB";
description = view.getSceneName() + "\n" + product.getName();
} else {
name = raster.getName();
description = raster.getDescription() + "\n" + product.getName();
legendKml = " <ScreenOverlay>\n"
+ " <name>Legend</name>\n"
+ " <Icon>\n"
+ " <href>legend.png</href>\n"
+ " </Icon>\n"
+ " <overlayXY x=\"0\" y=\"1\" xunits=\"fraction\" yunits=\"fraction\" />\n"
+ " <screenXY x=\"0\" y=\"1\" xunits=\"fraction\" yunits=\"fraction\" />\n"
+ " </ScreenOverlay>\n";
}
return "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
+ "<kml xmlns=\"http://earth.google.com/kml/2.0\">\n"
+ "<Document>\n"
+ " <name>" + name + "</name>\n"
+ " <description>" + description + "</description>\n"
+ " <GroundOverlay>\n"
+ " <name>Raster data</name>\n"
+ " <LatLonBox>\n"
+ " <north>" + upperLeftGP.getLat() + "</north>\n"
+ " <south>" + lowerRightGP.getLat() + "</south>\n"
+ " <east>" + eastLon + "</east>\n"
+ " <west>" + upperLeftGP.getLon() + "</west>\n"
+ " </LatLonBox>\n"
+ " <Icon>\n"
+ " <href>" + imageName + "</href>\n"
+ " </Icon>\n"
+ " </GroundOverlay>\n"
+ legendKml
+ pinKml
+ "</Document>\n"
+ "</kml>\n";
}
private static ImageLegend initImageLegend(RasterDataNode raster) {
ImageLegend imageLegend = new ImageLegend(raster.getImageInfo(), raster);
imageLegend.setHeaderText(getLegendHeaderText(raster));
imageLegend.setOrientation(ImageLegend.VERTICAL);
imageLegend.setBackgroundTransparency(0.0f);
imageLegend.setBackgroundTransparencyEnabled(true);
imageLegend.setAntialiasing(true);
return imageLegend;
}
private static String getLegendHeaderText(RasterDataNode raster) {
String unit = raster.getUnit() != null ? raster.getUnit() : "-";
unit = unit.replace('*', ' ');
return "(" + unit + ")";
}
private static class SaveKMLSwingWorker extends ProgressMonitorSwingWorker {
private final SnapApp snapApp;
private final ProductSceneView view;
private final File file;
SaveKMLSwingWorker(SnapApp snapApp, String message, ProductSceneView view, File file) {
super(snapApp.getMainFrame(), message);
this.snapApp = snapApp;
this.view = view;
this.file = file;
}
@Override
protected Object doInBackground(ProgressMonitor pm) throws Exception {
try {
final String message = String.format("Saving image as %s...", file.getPath());
pm.beginTask(message, view.isRGB() ? 4 : 3);
snapApp.setStatusBarMessage(message);
snapApp.getMainFrame().setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
final Dimension dimension = new Dimension(view.getProduct().getSceneRasterWidth(),
view.getProduct().getSceneRasterHeight());
RenderedImage image = ExportImageAction.createImage(view, true, dimension, true, true);
pm.worked(1);
try (ZipOutputStream outStream = new ZipOutputStream(new FileOutputStream(file))) {
outStream.putNextEntry(new ZipEntry(OVERLAY_KML));
final String kmlContent = formatKML(view, OVERLAY_PNG);
outStream.write(kmlContent.getBytes());
pm.worked(1);
outStream.putNextEntry(new ZipEntry(OVERLAY_PNG));
ImageEncoder encoder = ImageCodec.createImageEncoder(IMAGE_TYPE, outStream, null);
encoder.encode(image);
pm.worked(1);
if (!view.isRGB()) {
outStream.putNextEntry(new ZipEntry(LEGEND_PNG));
encoder = ImageCodec.createImageEncoder(IMAGE_TYPE, outStream, null);
encoder.encode(createImageLegend(view.getRaster()));
pm.worked(1);
}
}
} catch (OutOfMemoryError ignored) {
Dialogs.showOutOfMemoryError("The image could not be exported."); /*I18N*/
} catch (Throwable e) {
snapApp.handleError("The Image could not be exported", e);
} finally {
snapApp.getMainFrame().setCursor(Cursor.getDefaultCursor());
snapApp.setStatusBarMessage("");
pm.done();
}
return null;
}
}
}