/* (c) 2014 Open Source Geospatial Foundation - all rights reserved
* (c) 2001 - 2013 OpenPlans
* This code is licensed under the GPL 2.0 license, available at the root
* application directory.
*/
package org.geoserver.wps.gs.download;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.apache.commons.io.IOUtils;
import org.geoserver.catalog.FeatureTypeInfo;
import org.geoserver.wps.ppio.ComplexPPIO;
import org.geoserver.wps.ppio.ProcessParameterIO;
import org.geoserver.wps.resource.WPSResourceManager;
import org.geotools.data.Parameter;
import org.geotools.data.simple.SimpleFeatureCollection;
import org.geotools.data.simple.SimpleFeatureSource;
import org.geotools.data.store.ReprojectingFeatureCollection;
import org.geotools.factory.GeoTools;
import org.geotools.filter.visitor.SimplifyingFilterVisitor;
import org.geotools.process.ProcessException;
import org.geotools.process.vector.ClipProcess;
import org.geotools.referencing.CRS;
import org.geotools.resources.coverage.FeatureUtilities;
import org.geotools.util.logging.Logging;
import org.opengis.filter.Filter;
import org.opengis.filter.spatial.Intersects;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import org.opengis.referencing.operation.MathTransform;
import org.opengis.util.ProgressListener;
import com.vividsolutions.jts.geom.Geometry;
/**
* The class that does the real work of checking if we are exceeeding the download limits for vector data. Also this class writes the features in the
* output file.
*
* @author Simone Giannecchini, GeoSolutions SAS
*
*/
class VectorDownload {
private static final Logger LOGGER = Logging.getLogger(VectorDownload.class);
/** The {@link DownloadServiceConfiguration} object containing the configured limits. */
private DownloadServiceConfiguration limits;
/** The resource manager for handling the used resources. */
private WPSResourceManager resourceManager;
/**
* Constructor, takes a {@link DownloadServiceConfiguration} and a {@link WPSResourceManager}.
*
* @param limits the {@link DownloadServiceConfiguration} to check for not exceeding the download limits.
* @param resourceManager the {@link WPSResourceManager} to handle generated resources
*/
public VectorDownload(DownloadServiceConfiguration limits, WPSResourceManager resourceManager) {
this.limits = limits;
this.resourceManager = resourceManager;
}
/**
* Extract vector data to a file, given the provided mime-type. This method does the following operations:
* <ul>
* <li>Reads and filter the features (if needed)</li>
* <li>Reprojects the features (if needed)</li>
* <li>Clips the features (if needed)</li>
* <li>Writes the result</li>
* <li>Cleanup the generated coverages</li>
* </ul>
*
*
* @param resourceInfo the {@link FeatureTypeInfo} to download from
* @param mimeType the mme-type for the requested output format
* @param roi the {@link Geometry} for the clip/intersection
* @param clip whether or not to clip the resulting data (useless for the moment)
* @param filter the {@link Filter} to load the data
* @param targetCRS the reproject {@link CoordinateReferenceSystem}
* @param progressListener
* @return a file, given the provided mime-type.
* @throws Exception
*/
public File execute(FeatureTypeInfo resourceInfo, String mimeType, Geometry roi, boolean clip,
Filter filter, CoordinateReferenceSystem targetCRS,
final ProgressListener progressListener) throws Exception {
// prepare native CRS
CoordinateReferenceSystem nativeCRS = DownloadUtilities.getNativeCRS(resourceInfo);
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.fine("Native CRS is " + nativeCRS.toWKT());
}
//
// STEP 0 - Push ROI back to native CRS (if ROI is provided)
//
ROIManager roiManager = null;
if (roi != null) {
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.log(Level.FINE, "Pushing ROI to native CRS");
}
CoordinateReferenceSystem roiCRS = (CoordinateReferenceSystem) roi.getUserData();
roiManager = new ROIManager(roi, roiCRS);
// set use nativeCRS
roiManager.useNativeCRS(nativeCRS);
}
//
// STEP 1 - Read and Filter
//
// access feature source and collection of features
final SimpleFeatureSource featureSource = (SimpleFeatureSource) resourceInfo
.getFeatureSource(null, GeoTools.getDefaultHints());
// basic filter preparation
Filter ra = Filter.INCLUDE;
if (filter != null) {
ra = filter;
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.log(Level.FINE, "Using filter " + ra);
}
}
// and with the ROI if we have one
SimpleFeatureCollection originalFeatures;
final boolean hasROI = roiManager != null;
if (hasROI) {
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.log(Level.FINE, "Adding Geometry filter with ROI");
}
final String dataGeomName = featureSource.getSchema().getGeometryDescriptor()
.getLocalName();
final Intersects intersectionFilter = FeatureUtilities.DEFAULT_FILTER_FACTORY
.intersects(FeatureUtilities.DEFAULT_FILTER_FACTORY.property(dataGeomName),
FeatureUtilities.DEFAULT_FILTER_FACTORY.literal(roiManager
.getSafeRoiInNativeCRS()));
ra = FeatureUtilities.DEFAULT_FILTER_FACTORY.and(ra, intersectionFilter);
}
// simplify filter
ra = (Filter) ra.accept(new SimplifyingFilterVisitor(), null);
// read
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.log(Level.FINE, "Reading the filtered features");
}
originalFeatures = featureSource.getFeatures(ra);
DownloadUtilities.checkIsEmptyFeatureCollection(originalFeatures);
//
// STEP 2 - Reproject feature collection
//
// do we need to reproject?
SimpleFeatureCollection reprojectedFeatures;
if (targetCRS != null && !CRS.equalsIgnoreMetadata(nativeCRS, targetCRS)) {
roiManager.useTargetCRS(targetCRS);
// testing reprojection...
final MathTransform targetTX = CRS.findMathTransform(nativeCRS, targetCRS, true);
if (!targetTX.isIdentity()) {
// avoid doing the transform if this is the identity
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.log(Level.FINE, "Reprojecting features");
}
reprojectedFeatures = new ReprojectingFeatureCollection(originalFeatures, targetCRS);
} else {
reprojectedFeatures = originalFeatures;
DownloadUtilities.checkIsEmptyFeatureCollection(reprojectedFeatures);
}
} else {
reprojectedFeatures = originalFeatures;
if (hasROI) {
roiManager.useTargetCRS(nativeCRS);
}
}
//
// STEP 3 - Clip in targetCRS
//
SimpleFeatureCollection clippedFeatures;
if (clip && roi != null) {
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.log(Level.FINE, "Clipping features");
}
final ClipProcess clipProcess = new ClipProcess();// TODO avoid unnecessary creation
clippedFeatures = clipProcess.execute(reprojectedFeatures,
roiManager.getSafeRoiInTargetCRS(), true);
// checks
DownloadUtilities.checkIsEmptyFeatureCollection(clippedFeatures);
} else {
clippedFeatures = reprojectedFeatures;
}
//
// STEP 4 - Write down respecting limits in bytes
//
// writing the output, making sure it is a zip
return writeVectorOutput(clippedFeatures, resourceInfo.getName(), mimeType);
}
/**
* Write vector output with the provided PPIO. It returns the {@link File} it writes to.
*
* @param features {@link SimpleFeatureCollection} containing the features to write
* @param name name of the feature source
* @param mimeType mimetype of the result
* @return a {@link File} containing the written features
* @throws Exception
*/
private File writeVectorOutput(final SimpleFeatureCollection features, final String name,
final String mimeType) throws Exception {
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.log(Level.FINE, "Writing features");
}
// Search a proper PPIO
ProcessParameterIO ppio_ = DownloadUtilities.find(new Parameter<SimpleFeatureCollection>(
"fakeParam", SimpleFeatureCollection.class), null, mimeType, false);
if (ppio_ == null) {
throw new ProcessException("Don't know how to encode in mime type " + mimeType);
} else if (!(ppio_ instanceof ComplexPPIO)) {
throw new ProcessException("Invalid PPIO found " + ppio_.getIdentifer());
}
// limits
long limit = DownloadServiceConfiguration.NO_LIMIT;
if (limits.getHardOutputLimit() > 0) {
limit = limits.getHardOutputLimit();
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.log(Level.FINE, "Hard output limits set to " + limit);
}
} else {
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.log(Level.FINE, "Hard output limit unset");
}
}
//
// Get fileName
//
String extension = "";
if (ppio_ instanceof ComplexPPIO) {
extension = "." + ((ComplexPPIO) ppio_).getFileExtension();
}
// create output file
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.log(Level.FINE, "Writing file in a temporary folder");
}
final File output = resourceManager.getTemporaryFile(extension);
// write checking limits
OutputStream os = null;
try {
// If limits are configured we must create an OutputStream that checks limits
final BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(
new FileOutputStream(output));
if (limit > DownloadServiceConfiguration.NO_LIMIT) {
os = new LimitedOutputStream(bufferedOutputStream, limit) {
@Override
protected void raiseError(long pSizeMax, long pCount) throws IOException {
IOException ioe = new IOException(
"Download Exceeded the maximum HARD allowed size!");
throw ioe;
}
};
} else {
os = bufferedOutputStream;
}
// write with PPIO
if (ppio_ instanceof ComplexPPIO) {
((ComplexPPIO) ppio_).encode(features, os);
}
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.log(Level.FINE, "Flushing stream");
}
os.flush();
} finally {
if (os != null) {
IOUtils.closeQuietly(os);
}
}
// return
return output;
}
}