/*
* 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.CrsGeoCoding;
import org.esa.snap.core.datamodel.VectorDataNode;
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.ui.UIUtils;
import org.geotools.data.DataStore;
import org.geotools.data.DataUtilities;
import org.geotools.data.DefaultTransaction;
import org.geotools.data.FeatureStore;
import org.geotools.data.shapefile.ShapefileDataStoreFactory;
import org.geotools.data.store.ReprojectingFeatureCollection;
import org.geotools.feature.FeatureCollection;
import org.geotools.feature.FeatureIterator;
import org.geotools.feature.simple.SimpleFeatureTypeBuilder;
import org.geotools.referencing.CRS;
import org.geotools.referencing.crs.DefaultGeographicCRS;
import org.opengis.feature.simple.SimpleFeature;
import org.opengis.feature.simple.SimpleFeatureType;
import org.opengis.feature.type.AttributeDescriptor;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
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.SwingWorker;
import java.awt.event.ActionEvent;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ExecutionException;
@ActionID(
category = "File",
id = "org.esa.snap.rcp.actions.file.export.ExportGeometryAction"
)
@ActionRegistration(
displayName = "#CTL_ExportGeometryAction_MenuText",
popupText = "#CTL_ExportGeometryAction_PopupText",
lazy = false
)
@ActionReferences({
@ActionReference(path = "Menu/File/Export/Other", position = 40),
@ActionReference(path = "Menu/Vector/Export"),
@ActionReference(path = "Context/Product/VectorDataNode", position = 208)
})
@NbBundle.Messages({
"CTL_ExportGeometryAction_MenuText=Geometry as Shape file",
"CTL_ExportGeometryAction_PopupText=Export Geometry as Shape file",
"CTL_ExportGeometryAction_DialogTitle=Export Geometry as ESRI Shapefile",
"CTL_ExportGeometryAction_ShortDescription=Exports the currently selected geometry as ESRI Shapefile."
})
public class ExportGeometryAction extends AbstractAction implements ContextAwareAction, LookupListener, HelpCtx.Provider {
private static final String ESRI_SHAPEFILE = "ESRI Shapefile";
private static final String FILE_EXTENSION_SHAPEFILE = ".shp";
private final Lookup.Result<VectorDataNode> result;
private final Lookup lookup;
private String HELP_ID = "exportShapefile";
private VectorDataNode vectorDataNode;
public ExportGeometryAction() {
this(Utilities.actionsGlobalContext());
}
public ExportGeometryAction(Lookup lookup) {
super(Bundle.CTL_ExportGeometryAction_MenuText());
this.lookup = lookup;
result = lookup.lookupResult(VectorDataNode.class);
result.addLookupListener(WeakListeners.create(LookupListener.class, this, result));
vectorDataNode = lookup.lookup(VectorDataNode.class);
setEnabled(vectorDataNode != null);
}
/*
* 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) {
return Dialogs.requestFileForSave(Bundle.CTL_ExportGeometryAction_DialogTitle(), false,
new SnapFileFilter(ESRI_SHAPEFILE, FILE_EXTENSION_SHAPEFILE, ESRI_SHAPEFILE),
FILE_EXTENSION_SHAPEFILE,
defaultFileName,
null,
"exportVectorDataNode.lastDir");
}
/////////////////////////////////////////////////////////////////////////
// Private implementations for the "export Mask Pixels" command
/////////////////////////////////////////////////////////////////////////
private static void exportVectorDataNode(VectorDataNode vectorNode, File file, ProgressMonitor pm) throws
IOException {
Map<Class<?>, List<SimpleFeature>> featureListMap = createGeometryToFeaturesListMap(vectorNode);
if (featureListMap.size() > 1) {
final String msg = "The selected geometry contains different types of shapes.\n" +
"Each type of shape will be exported as a separate shapefile.";
Dialogs.showInformation(Bundle.CTL_ExportGeometryAction_DialogTitle(),
msg, ExportGeometryAction.class.getName() + ".exportInfo");
}
Set<Map.Entry<Class<?>, List<SimpleFeature>>> entries = featureListMap.entrySet();
pm.beginTask("Writing ESRI Shapefiles...", featureListMap.size());
try {
for (Map.Entry<Class<?>, List<SimpleFeature>> entry : entries) {
writeEsriShapefile(entry.getKey(), entry.getValue(), file);
pm.worked(1);
}
} finally {
pm.done();
}
}
private static void writeEsriShapefile(Class<?> geomType, List<SimpleFeature> features, File file) throws IOException {
String geomName = geomType.getSimpleName();
String basename = file.getName();
if (basename.endsWith(FILE_EXTENSION_SHAPEFILE)) {
basename = basename.substring(0, basename.length() - 4);
}
File file1 = new File(file.getParentFile(), basename + "_" + geomName + FILE_EXTENSION_SHAPEFILE);
ShapefileDataStoreFactory factory = new ShapefileDataStoreFactory();
Map map = Collections.singletonMap("url", file1.toURI().toURL());
DataStore dataStore = factory.createNewDataStore(map);
SimpleFeature simpleFeature = features.get(0);
SimpleFeatureType simpleFeatureType = changeGeometryType(simpleFeature.getType(), geomType);
String typeName = simpleFeatureType.getName().getLocalPart();
dataStore.createSchema(simpleFeatureType);
FeatureStore<SimpleFeatureType, SimpleFeature> featureStore = (FeatureStore<SimpleFeatureType, SimpleFeature>) dataStore.getFeatureSource(
typeName);
DefaultTransaction transaction = new DefaultTransaction("X");
featureStore.setTransaction(transaction);
final FeatureCollection<SimpleFeatureType, SimpleFeature> featureCollection = DataUtilities.collection(
features);
featureStore.addFeatures(featureCollection);
try {
transaction.commit();
} catch (IOException e) {
transaction.rollback();
throw e;
} finally {
transaction.close();
}
}
private static Map<Class<?>, List<SimpleFeature>> createGeometryToFeaturesListMap(VectorDataNode vectorNode) {
FeatureCollection<SimpleFeatureType, SimpleFeature> featureCollection = vectorNode.getFeatureCollection();
CoordinateReferenceSystem crs = vectorNode.getFeatureType().getCoordinateReferenceSystem();
if (crs == null) { // for pins and GCPs crs is null --> assume image crs
crs = vectorNode.getProduct().getSceneGeoCoding().getImageCRS();
}
final CoordinateReferenceSystem modelCrs;
if (vectorNode.getProduct().getSceneGeoCoding() instanceof CrsGeoCoding) {
modelCrs = vectorNode.getProduct().getSceneCRS();
} else {
modelCrs = DefaultGeographicCRS.WGS84;
}
if (!CRS.equalsIgnoreMetadata(crs, modelCrs)) { // we have to reproject the features
featureCollection = new ReprojectingFeatureCollection(featureCollection, crs, modelCrs);
}
Map<Class<?>, List<SimpleFeature>> featureListMap = new HashMap<>();
final FeatureIterator<SimpleFeature> featureIterator = featureCollection.features();
while (featureIterator.hasNext()) {
SimpleFeature feature = featureIterator.next();
Object defaultGeometry = feature.getDefaultGeometry();
Class<?> geometryType = defaultGeometry.getClass();
List<SimpleFeature> featureList = featureListMap.get(geometryType);
if (featureList == null) {
featureList = new ArrayList<>();
featureListMap.put(geometryType, featureList);
}
featureList.add(feature);
}
return featureListMap;
}
private static SimpleFeatureType changeGeometryType(SimpleFeatureType original, Class<?> geometryType) {
SimpleFeatureTypeBuilder sftb = new SimpleFeatureTypeBuilder();
sftb.setCRS(original.getCoordinateReferenceSystem());
sftb.setDefaultGeometry(original.getGeometryDescriptor().getLocalName());
sftb.add(original.getGeometryDescriptor().getLocalName(), geometryType);
for (AttributeDescriptor descriptor : original.getAttributeDescriptors()) {
if (!original.getGeometryDescriptor().getLocalName().equals(descriptor.getLocalName())) {
sftb.add(descriptor);
}
}
sftb.setName("FT_" + geometryType.getSimpleName());
return sftb.buildFeatureType();
}
@Override
public void actionPerformed(ActionEvent e) {
exportVectorDataNode();
}
@Override
public HelpCtx getHelpCtx() {
return new HelpCtx(HELP_ID);
}
@Override
public Action createContextAwareInstance(Lookup actionContext) {
return new ExportGeometryAction(actionContext);
}
@Override
public void resultChanged(LookupEvent lookupEvent) {
vectorDataNode = lookup.lookup(VectorDataNode.class);
setEnabled(vectorDataNode != null);
}
/**
* Performs the actual "export Mask Pixels" command.
*/
private void exportVectorDataNode() {
SnapApp snapApp = SnapApp.getDefault();
if (vectorDataNode.getFeatureCollection().isEmpty()) {
Dialogs.showInformation(Bundle.CTL_ExportGeometryAction_DialogTitle(),
"The selected geometry is empty. Nothing to export.", null);
return;
}
final File file = promptForFile(vectorDataNode.getName());
if (file == null) {
return;
}
final SwingWorker<Exception, Object> swingWorker = new ExportVectorNodeSwingWorker(snapApp, vectorDataNode, file);
UIUtils.setRootFrameWaitCursor(snapApp.getMainFrame());
snapApp.setStatusBarMessage("Exporting Geometry...");
swingWorker.execute();
}
private static class ExportVectorNodeSwingWorker extends ProgressMonitorSwingWorker<Exception, Object> {
private final SnapApp snapApp;
private final VectorDataNode vectorDataNode;
private final File file;
private ExportVectorNodeSwingWorker(SnapApp snapApp, VectorDataNode vectorDataNode, File file) {
super(snapApp.getMainFrame(), Bundle.CTL_ExportGeometryAction_DialogTitle());
this.snapApp = snapApp;
this.vectorDataNode = vectorDataNode;
this.file = file;
}
@Override
protected Exception doInBackground(ProgressMonitor pm) throws Exception {
try {
exportVectorDataNode(vectorDataNode, file, pm);
} catch (IOException e) {
return e;
}
return null;
}
/**
* Called on the event dispatching thread (not on the worker thread) after the <code>construct</code> method
* has returned.
*/
@Override
public void done() {
Exception exception = null;
try {
UIUtils.setRootFrameDefaultCursor(SnapApp.getDefault().getMainFrame());
snapApp.setStatusBarMessage("");
exception = get();
} catch (InterruptedException e) {
exception = e;
} catch (ExecutionException e) {
exception = e;
} finally {
if (exception != null) {
exception.printStackTrace();
Dialogs.showError(Bundle.CTL_ExportGeometryAction_DialogTitle(),
"Can not export geometry.\n" + exception.getMessage());
}
}
}
}
}