/* (c) 2014 - 2016 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.File; import java.io.IOException; import java.util.List; import java.util.logging.Level; import java.util.logging.Logger; import javax.imageio.stream.ImageOutputStream; import javax.media.jai.Interpolation; import org.apache.commons.lang.ArrayUtils; import org.geoserver.catalog.CoverageInfo; import org.geoserver.data.util.CoverageUtils; import org.geoserver.platform.resource.Resource; import org.geoserver.wps.WPSException; import org.geoserver.wps.ppio.ComplexPPIO; import org.geoserver.wps.ppio.ProcessParameterIO; import org.geoserver.wps.resource.GridCoverageResource; import org.geoserver.wps.resource.WPSResourceManager; import org.geotools.coverage.grid.GridCoverage2D; import org.geotools.coverage.grid.GridGeometry2D; import org.geotools.coverage.grid.io.AbstractGridFormat; import org.geotools.coverage.grid.io.GridCoverage2DReader; import org.geotools.coverage.processing.Operations; import org.geotools.data.Parameter; import org.geotools.geometry.jts.JTS; import org.geotools.geometry.jts.ReferencedEnvelope; import org.geotools.process.ProcessException; import org.geotools.process.raster.BandSelectProcess; import org.geotools.process.raster.CropCoverage; import org.geotools.referencing.CRS; import org.geotools.resources.coverage.CoverageUtilities; import org.geotools.resources.coverage.FeatureUtilities; import org.geotools.util.logging.Logging; import org.opengis.filter.Filter; import org.opengis.parameter.GeneralParameterDescriptor; import org.opengis.parameter.GeneralParameterValue; import org.opengis.parameter.ParameterValueGroup; import org.opengis.referencing.crs.CoordinateReferenceSystem; import org.opengis.referencing.operation.MathTransform; import org.opengis.util.ProgressListener; import org.springframework.context.ApplicationContext; import com.vividsolutions.jts.geom.Geometry; import com.vividsolutions.jts.geom.GeometryFactory; import com.vividsolutions.jts.geom.Polygon; import com.vividsolutions.jts.geom.PrecisionModel; import it.geosolutions.imageio.stream.output.ImageOutputStreamAdapter; import it.geosolutions.io.output.adapter.OutputStreamAdapter; /** * Implements the download services for raster data. If limits are configured this class will use {@link LimitedImageOutputStream}, which raises an * exception when the download size exceeded the limits. * * @author Simone Giannecchini, GeoSolutions SAS * */ class RasterDownload { private static final Logger LOGGER = Logging.getLogger(RasterDownload.class); /** The {@link DownloadServiceConfiguration} object containing the configured limits. */ private DownloadServiceConfiguration limits; /** The resource manager for handling the used resources. */ private WPSResourceManager resourceManager; /** * The application context used to look-up PPIO factories */ private ApplicationContext context; /** * Constructor, takes a {@link DownloadEstimatorProcess}. * * @param limits the {@link DownloadEstimatorProcess} to check for not exceeding the download * limits. * @param resourceManager the {@link WPSResourceManager} to handl generated resources * @param context */ public RasterDownload(DownloadServiceConfiguration limits, WPSResourceManager resourceManager, ApplicationContext context) { this.limits = limits; this.resourceManager = resourceManager; this.context = context; } /** * This method does the following operations: * <ul> * <li>Uses only those bands specified by indices (if defined)</li> * <li>Reprojection of the coverage (if needed)</li> * <li>Clips the coverage (if needed)</li> * <li>Scales the coverage to match the target size (if needed)</li> * <li>Writes the result</li> * <li>Cleanup the generated coverages</li> * </ul> * * @param mimeType mimetype of the result * @param progressListener listener to use for logging the operations * @param coverageInfo resource associated to the Coverage * @param roi input ROI object * @param targetCRS CRS of the file to write * @param clip indicates if the clipping geometry must be exactly that of the ROI or simply its envelope * @param interpolation interpolation method to use when reprojecting / scaling * @param targetSizeX the size of the target image along the X axis * @param targetSizeY the size of the target image along the Y axis * @param bandIndices the indices of the bands used for the final result * @param filter the {@link Filter} to load the data * */ public Resource execute(String mimeType, final ProgressListener progressListener, CoverageInfo coverageInfo, Geometry roi, CoordinateReferenceSystem targetCRS, boolean clip, Filter filter, Interpolation interpolation, Integer targetSizeX, Integer targetSizeY, int[] bandIndices) throws Exception { GridCoverage2D scaledGridCoverage = null, clippedGridCoverage = null, reprojectedGridCoverage = null, bandFilteredCoverage = null, originalGridCoverage = null; try { // // look for output extension. Tiff/tif/geotiff will be all treated as GeoTIFF // // // ---> READ FROM NATIVE RESOLUTION <-- // // prepare native CRS CoordinateReferenceSystem nativeCRS = DownloadUtilities.getNativeCRS(coverageInfo); 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"); } final CoordinateReferenceSystem roiCRS = (CoordinateReferenceSystem) roi .getUserData(); roiManager = new ROIManager(roi, roiCRS); } // // STEP 1 - Reproject if needed // boolean reproject = false; MathTransform reprojectionTrasform = null; if (targetCRS != null && !CRS.equalsIgnoreMetadata(nativeCRS, targetCRS)) { if (LOGGER.isLoggable(Level.FINE)) { LOGGER.log(Level.FINE, "Checking if reprojection is needed"); } // testing reprojection... reprojectionTrasform = CRS.findMathTransform(nativeCRS, targetCRS, true); if (!reprojectionTrasform.isIdentity()) { // avoid doing the transform if this is the identity reproject = true; if (LOGGER.isLoggable(Level.FINE)) { LOGGER.log(Level.FINE, "Reprojection needed"); } } } else { targetCRS = nativeCRS; } // get a reader for this CoverageInfo if (LOGGER.isLoggable(Level.FINE)) { LOGGER.log(Level.FINE, "Getting reader for the coverage"); } final GridCoverage2DReader reader = (GridCoverage2DReader) coverageInfo .getGridCoverageReader(null, null); final ParameterValueGroup readParametersDescriptor = reader.getFormat() .getReadParameters(); final List<GeneralParameterDescriptor> parameterDescriptors = readParametersDescriptor .getDescriptor().descriptors(); // get the configured metadata for this coverage without GeneralParameterValue[] readParameters = CoverageUtils.getParameters( readParametersDescriptor, coverageInfo.getParameters(), false); // merge support for filter if (filter != null) { if (LOGGER.isLoggable(Level.FINE)) { LOGGER.log(Level.FINE, "Add the filter"); } readParameters = CoverageUtils.mergeParameter(parameterDescriptors, readParameters, filter, "FILTER", "Filter"); } // read GridGeometry preparation and scaling setup ScaleToTarget scaling = null; if (roi != null) { // set crs in roi manager roiManager.useNativeCRS(reader.getCoordinateReferenceSystem()); roiManager.useTargetCRS(targetCRS); if (LOGGER.isLoggable(Level.FINE)) { LOGGER.log(Level.FINE, "Preparing the GridGeometry for cropping input layer with ROI"); } // create GridGeometry final ReferencedEnvelope roiEnvelope = new ReferencedEnvelope(roiManager .getSafeRoiInNativeCRS().getEnvelopeInternal(), // safe envelope nativeCRS); final Polygon originalEnvelopeAsPolygon = FeatureUtilities.getPolygon(reader.getOriginalEnvelope(), new GeometryFactory(new PrecisionModel(PrecisionModel.FLOATING))); originalEnvelopeAsPolygon.setUserData(nativeCRS); final ReferencedEnvelope originalEnvelope = JTS.toEnvelope(originalEnvelopeAsPolygon); // calculate intersection between original envelope and ROI, as blindly trusting // the ROI may give issues with scaling, if target size is not specified for // both X and Y dimensions final ReferencedEnvelope intersection = originalEnvelope.intersection(roiEnvelope); // take scaling into account scaling = new ScaleToTarget(reader, intersection); scaling.setTargetSize(targetSizeX, targetSizeY); GridGeometry2D gg2D = scaling.getGridGeometry(); // TODO make sure the GridRange is not empty, depending on the resolution it might happen readParameters = CoverageUtils.mergeParameter(parameterDescriptors, readParameters, gg2D, AbstractGridFormat.READ_GRIDGEOMETRY2D.getName().getCode()); } else { // we are reading the full coverage scaling = new ScaleToTarget(reader); scaling.setTargetSize(targetSizeX, targetSizeY); } // make sure we work in streaming fashion readParameters = CoverageUtils.mergeParameter(parameterDescriptors, readParameters, Boolean.TRUE, AbstractGridFormat.USE_JAI_IMAGEREAD.getName().getCode()); // --> READ originalGridCoverage = reader.read(readParameters); // check, the reader might have returned a null coverage if(originalGridCoverage == null) { throw new WPSException("The reader did not return any data for current input " + "parameters. It normally means there is nothing there, or the data got filtered out by the ROI or filter"); } // // STEP 0 - Check for bands, select only those specified // if (bandIndices!=null && bandIndices.length>0){ //check band indices are valid int sampleDimensionsNumber = originalGridCoverage.getNumSampleDimensions(); for (int i:bandIndices){ if (i<0 || i>=sampleDimensionsNumber){ throw new WPSException( "Band index "+i+" is invalid for the current input raster. " + "This raster contains "+sampleDimensionsNumber+" band" + (sampleDimensionsNumber>1?"s":"")); } } BandSelectProcess bandSelectProcess = new BandSelectProcess(); //using null for the VisibleSampleDimension parameter of BandSelectProcess.execute. //GeoTools BandSelector2D takes care of remapping visible band index //or assigns it to first band in order if remapping is not possible bandFilteredCoverage = bandSelectProcess.execute( originalGridCoverage, bandIndices, null); }else{ bandFilteredCoverage = originalGridCoverage; } // // STEP 1 - Reproject if needed // if (reproject) { if (LOGGER.isLoggable(Level.FINE)) { LOGGER.log(Level.FINE, "Reprojecting the layer"); } // avoid doing the transform if this is the identity reprojectedGridCoverage = (GridCoverage2D) Operations.DEFAULT.resample( bandFilteredCoverage, targetCRS, null, interpolation); } else { reprojectedGridCoverage = bandFilteredCoverage; } // // STEP 2 - Clip if needed // // we need to push the ROI to the final CRS to crop or CLIP if (roi != null) { if (LOGGER.isLoggable(Level.FINE)) { LOGGER.log(Level.FINE, "Cropping the layer"); } // Crop or Clip final CropCoverage cropCoverage = new CropCoverage(); // TODO avoid creation if (clip) { // clipping means carefully following the ROI shape clippedGridCoverage = cropCoverage.execute(reprojectedGridCoverage, roiManager.getSafeRoiInTargetCRS(), progressListener); } else { // use envelope of the ROI to simply crop and not clip the raster. This is important since when // reprojecting we might read a bit more than needed! clippedGridCoverage = cropCoverage.execute(reprojectedGridCoverage, roiManager.getSafeRoiInTargetCRS(), progressListener); } if(clippedGridCoverage == null) { throw new WPSException("No data left after applying the ROI. This means there " + "is source data, but none matching the requested ROI"); } } else { // do nothing clippedGridCoverage = reprojectedGridCoverage; } // // STEP 3 - scale to target size, if needed // if (interpolation != null) { scaling.setInterpolation(interpolation); } if (LOGGER.isLoggable(Level.FINE)) { LOGGER.log(Level.FINE, "Scaling the layer"); } // scaling and/or interpolation scaledGridCoverage = scaling.scale(clippedGridCoverage); // // STEP 4 - Writing // return writeRaster(mimeType, coverageInfo, scaledGridCoverage); } finally { if (originalGridCoverage != null) { resourceManager.addResource(new GridCoverageResource(originalGridCoverage)); } if (reprojectedGridCoverage != null) { resourceManager.addResource(new GridCoverageResource(reprojectedGridCoverage)); } if (clippedGridCoverage != null) { resourceManager.addResource(new GridCoverageResource(clippedGridCoverage)); } if (scaledGridCoverage != null) { resourceManager.addResource(new GridCoverageResource(scaledGridCoverage)); } } } /** * Writes the providede GridCoverage as a GeoTiff file. * * @param mimeType result mimetype * @param coverageInfo resource associated to the input coverage * @param gridCoverage gridcoverage to write * @return a {@link File} that points to the GridCoverage we wrote. * */ private Resource writeRaster(String mimeType, CoverageInfo coverageInfo, GridCoverage2D gridCoverage) throws Exception { if (LOGGER.isLoggable(Level.FINE)) { LOGGER.log(Level.FINE, "Writing raster"); } // 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"); } } // Search a proper PPIO Parameter<GridCoverage2D> gridParam = new Parameter<GridCoverage2D>("fakeParam", GridCoverage2D.class); ProcessParameterIO ppio_ = DownloadUtilities.find(gridParam, context, 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()); } final ComplexPPIO complexPPIO = (ComplexPPIO) ppio_; String extension = complexPPIO.getFileExtension(); // writing the output to a temporary folder if (LOGGER.isLoggable(Level.FINE)) { LOGGER.log(Level.FINE, "Writing file in a temporary folder"); } final Resource output = resourceManager.getTemporaryResource("." + extension); // the limit output stream will throw an exception if the process is trying to writer more than the max allowed bytes final ImageOutputStream fileImageOutputStreamExtImpl = new ImageOutputStreamAdapter( output.out()); ImageOutputStream os = null; // write try { // If limit is defined, LimitedImageOutputStream is used if (limit > DownloadServiceConfiguration.NO_LIMIT) { os = new LimitedImageOutputStream(fileImageOutputStreamExtImpl, limit) { @Override protected void raiseError(long pSizeMax, long pCount) throws IOException { IOException e = new IOException( "Download Exceeded the maximum HARD allowed size!"); throw e; } }; } else { os = fileImageOutputStreamExtImpl; } // Encoding the GridCoverage complexPPIO.encode(gridCoverage, new OutputStreamAdapter(os)); os.flush(); } finally { try { if (os != null) { os.close(); } } catch (Exception e) { if (LOGGER.isLoggable(Level.FINE)) { LOGGER.log(Level.FINE, e.getLocalizedMessage(), e); } } } return output; } }