Commit a880ed02 authored by Stefano Costa's avatar Stefano Costa
Browse files

[GEOS-7214] Add scaling support to WPS gs:Download process

parent dd4f5e00
/* (c) 2014 Open Source Geospatial Foundation - all rights reserved
/* (c) 2014 - 2015 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.
......@@ -66,6 +66,8 @@ public class DownloadEstimatorProcess implements GSProcess {
* @param roiCRS the roi crs
* @param roi the roi
* @param clip the crop to geometry
* @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 progressListener the progress listener
* @return the boolean
* @throws Exception
......@@ -78,6 +80,8 @@ public class DownloadEstimatorProcess implements GSProcess {
@DescribeParameter(name = "RoiCRS", min = 0, description = "Region Of Interest CRS") CoordinateReferenceSystem roiCRS,
@DescribeParameter(name = "ROI", min = 0, description = "Region Of Interest") Geometry roi,
@DescribeParameter(name = "cropToROI", min = 0, description = "Crop to ROI") Boolean clip,
@DescribeParameter(name = "targetSizeX", min = 0, minValue = 1, description = "X Size of the Target Image (applies to raster data only)") Integer targetSizeX,
@DescribeParameter(name = "targetSizeY", min = 0, minValue = 1, description = "Y Size of the Target Image (applies to raster data only)") Integer targetSizeY,
ProgressListener progressListener) throws Exception {
//
......@@ -153,7 +157,7 @@ public class DownloadEstimatorProcess implements GSProcess {
}
final CoverageInfo coverage = (CoverageInfo) resourceInfo;
return new RasterEstimator(limits).execute(progressListener, coverage, roi, targetCRS,
clip, filter);
clip, filter, targetSizeX, targetSizeY);
}
if (LOGGER.isLoggable(Level.FINE)) {
......
/* (c) 2014 Open Source Geospatial Foundation - all rights reserved
/* (c) 2014 - 2015 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.
......@@ -12,6 +12,9 @@ import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.media.jai.Interpolation;
import javax.media.jai.JAI;
import org.apache.commons.io.IOUtils;
import org.geoserver.catalog.Catalog;
import org.geoserver.catalog.CoverageInfo;
......@@ -27,6 +30,7 @@ import org.geotools.process.factory.DescribeParameter;
import org.geotools.process.factory.DescribeProcess;
import org.geotools.process.factory.DescribeResult;
import org.geotools.process.gs.GSProcess;
import org.geotools.resources.image.ImageUtilities;
import org.geotools.util.Utilities;
import org.geotools.util.logging.Logging;
import org.opengis.filter.Filter;
......@@ -92,6 +96,9 @@ public class DownloadProcess implements GSProcess, ApplicationContextAware {
* @param roiCRS the roi crs
* @param roi the roi
* @param clip the crop to geometry
* @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 progressListener the progress listener
* @return the file
* @throws ProcessException the process exception
......@@ -105,6 +112,9 @@ public class DownloadProcess implements GSProcess, ApplicationContextAware {
@DescribeParameter(name = "RoiCRS", min = 0, description = "Optional Region Of Interest CRS") CoordinateReferenceSystem roiCRS,
@DescribeParameter(name = "ROI", min = 0, description = "Optional Region Of Interest (Polygon)") Geometry roi,
@DescribeParameter(name = "cropToROI", min = 0, description = "Crop to ROI") Boolean clip,
@DescribeParameter(name = "interpolation", description = "Interpolation function to use when reprojecting / scaling raster data. Values are NEAREST (default), BILINEAR, BICUBIC2, BICUBIC", min = 0) Interpolation interpolation,
@DescribeParameter(name = "targetSizeX", min = 0, minValue = 1, description = "X Size of the Target Image (applies to raster data only)") Integer targetSizeX,
@DescribeParameter(name = "targetSizeY", min = 0, minValue = 1, description = "Y Size of the Target Image (applies to raster data only)") Integer targetSizeY,
final ProgressListener progressListener) throws ProcessException {
try {
......@@ -138,14 +148,24 @@ public class DownloadProcess implements GSProcess, ApplicationContextAware {
roi.setUserData(roiCRS);
}
// set default interpolation value
if (interpolation == null) {
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.log(Level.FINE,
"Interpolation parameter not specified, using default (Nearest Neighbor)");
}
interpolation = (Interpolation) ImageUtilities.NN_INTERPOLATION_HINT
.get(JAI.KEY_INTERPOLATION);
}
//
// do we respect limits?
//
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.log(Level.FINE, "Running the estimator");
}
if (!estimator.execute(layerName, filter, targetCRS, roiCRS, roi, clip,
progressListener)) {
if (!estimator.execute(layerName, filter, targetCRS, roiCRS, roi, clip, targetSizeX,
targetSizeY, progressListener)) {
throw new IllegalArgumentException("Download Limits Exceeded. Unable to proceed!");
}
......@@ -201,9 +221,9 @@ public class DownloadProcess implements GSProcess, ApplicationContextAware {
//
CoverageInfo cInfo = (CoverageInfo) resourceInfo;
// convert/reproject/crop if needed the coverage
internalOutput = new RasterDownload(limits, resourceManager, context)
.execute(mimeType,
progressListener, cInfo, roi, targetCRS, clip, filter);
internalOutput = new RasterDownload(limits, resourceManager, context).execute(
mimeType, progressListener, cInfo, roi, targetCRS, clip, filter,
interpolation, targetSizeX, targetSizeY);
} else {
// wrong type
......
/* (c) 2014 Open Source Geospatial Foundation - all rights reserved
/* (c) 2014 - 2015 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 it.geosolutions.imageio.stream.output.FileImageOutputStreamExtImpl;
import it.geosolutions.io.output.adapter.OutputStreamAdapter;
import java.io.File;
import java.io.IOException;
import java.util.List;
......@@ -12,6 +15,7 @@ import java.util.logging.Level;
import java.util.logging.Logger;
import javax.imageio.stream.ImageOutputStream;
import javax.media.jai.Interpolation;
import org.geoserver.catalog.CoverageInfo;
import org.geoserver.data.util.CoverageUtils;
......@@ -25,26 +29,26 @@ 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.factory.GeoTools;
import org.geotools.geometry.jts.JTS;
import org.geotools.geometry.jts.ReferencedEnvelope;
import org.geotools.process.ProcessException;
import org.geotools.process.raster.CropCoverage;
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.parameter.GeneralParameterDescriptor;
import org.opengis.parameter.GeneralParameterValue;
import org.opengis.parameter.ParameterValueGroup;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import org.opengis.referencing.datum.PixelInCell;
import org.opengis.referencing.operation.MathTransform;
import org.opengis.util.ProgressListener;
import org.springframework.context.ApplicationContext;
import com.vividsolutions.jts.geom.Geometry;
import it.geosolutions.imageio.stream.output.FileImageOutputStreamExtImpl;
import it.geosolutions.io.output.adapter.OutputStreamAdapter;
import com.vividsolutions.jts.geom.GeometryFactory;
import com.vividsolutions.jts.geom.Polygon;
import com.vividsolutions.jts.geom.PrecisionModel;
/**
* Implements the download services for raster data. If limits are configured this class will use {@link LimitedImageOutputStream}, which raises an
......@@ -88,6 +92,7 @@ class RasterDownload {
* <ul>
* <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>
......@@ -98,15 +103,19 @@ class RasterDownload {
* @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 filter the {@link Filter} to load the data
* @return
* @throws Exception
*/
public File execute(String mimeType, final ProgressListener progressListener,
CoverageInfo coverageInfo, Geometry roi, CoordinateReferenceSystem targetCRS,
boolean clip, Filter filter) throws Exception {
boolean clip, Filter filter, Interpolation interpolation, Integer targetSizeX,
Integer targetSizeY) throws Exception {
GridCoverage2D clippedGridCoverage = null, reprojectedGridCoverage = null, originalGridCoverage = null;
GridCoverage2D scaledGridCoverage = null, clippedGridCoverage = null, reprojectedGridCoverage = null, originalGridCoverage = null;
try {
//
......@@ -180,7 +189,8 @@ class RasterDownload {
readParameters = CoverageUtils.mergeParameter(parameterDescriptors, readParameters,
filter, "FILTER", "Filter");
}
// read GridGeometry preparation
// read GridGeometry preparation and scaling setup
ScaleToTarget scaling = null;
if (roi != null) {
// set crs in roi manager
roiManager.useNativeCRS(reader.getCoordinateReferenceSystem());
......@@ -193,12 +203,26 @@ class RasterDownload {
final ReferencedEnvelope roiEnvelope = new ReferencedEnvelope(roiManager
.getSafeRoiInNativeCRS().getEnvelopeInternal(), // safe envelope
nativeCRS);
GridGeometry2D gg2D = new GridGeometry2D(PixelInCell.CELL_CENTER,
reader.getOriginalGridToWorld(PixelInCell.CELL_CENTER), roiEnvelope,
GeoTools.getDefaultHints());
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,
......@@ -216,7 +240,7 @@ class RasterDownload {
}
// avoid doing the transform if this is the identity
reprojectedGridCoverage = (GridCoverage2D) Operations.DEFAULT.resample(
originalGridCoverage, targetCRS);
originalGridCoverage, targetCRS, null, interpolation);
} else {
reprojectedGridCoverage = originalGridCoverage;
......@@ -239,8 +263,6 @@ class RasterDownload {
} 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!
final Geometry polygon = roiManager.getSafeRoiInTargetCRS();
polygon.setUserData(targetCRS);
clippedGridCoverage = cropCoverage.execute(reprojectedGridCoverage,
roiManager.getSafeRoiInTargetCRS(), progressListener);
}
......@@ -250,9 +272,21 @@ class RasterDownload {
}
//
// STEP 3 - Writing
// STEP 3 - scale to target size, if needed
//
return writeRaster(mimeType, coverageInfo, clippedGridCoverage);
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));
......@@ -263,6 +297,9 @@ class RasterDownload {
if (clippedGridCoverage != null) {
resourceManager.addResource(new GridCoverageResource(clippedGridCoverage));
}
if (scaledGridCoverage != null) {
resourceManager.addResource(new GridCoverageResource(scaledGridCoverage));
}
}
}
......
/* (c) 2014 Open Source Geospatial Foundation - all rights reserved
/* (c) 2014 - 2015 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.awt.geom.Rectangle2D;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.geoserver.catalog.CoverageInfo;
import org.geotools.coverage.grid.GridEnvelope2D;
import org.geotools.coverage.grid.GridGeometry2D;
import org.geotools.coverage.grid.io.GridCoverage2DReader;
import org.geotools.geometry.jts.JTS;
import org.geotools.referencing.operation.transform.AffineTransform2D;
import org.geotools.geometry.jts.ReferencedEnvelope;
import org.geotools.resources.coverage.FeatureUtilities;
import org.geotools.util.logging.Logging;
import org.opengis.filter.Filter;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import org.opengis.referencing.datum.PixelInCell;
import org.opengis.util.ProgressListener;
import com.vividsolutions.jts.geom.Geometry;
......@@ -59,11 +57,13 @@ class RasterEstimator {
* @param targetCRS the reproject {@link CoordinateReferenceSystem} (useless for the moment)
* @param clip whether or not to clip the resulting data (useless for the moment)
* @param filter the {@link Filter} to load the data
* @param targetSizeX the size of the target image along the X axis
* @param targetSizeY the size of the target image along the Y axis
* @return
*/
public boolean execute(final ProgressListener progressListener, CoverageInfo coverageInfo,
Geometry roi, CoordinateReferenceSystem targetCRS, boolean clip, Filter filter)
throws Exception {
Geometry roi, CoordinateReferenceSystem targetCRS, boolean clip, Filter filter,
Integer targetSizeX, Integer targetSizeY) throws Exception {
//
// Do we need to do anything?
......@@ -110,8 +110,10 @@ class RasterEstimator {
// Area to read in pixel
final double areaRead;
// If ROI is present, then the coverage BBOX is cropped with the ROI geometry
// take scaling into account
ScaleToTarget scaling = null;
if (roi != null) {
// If ROI is present, then the coverage BBOX is cropped with the ROI geometry
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.log(Level.FINE, "Reprojecting ROI");
}
......@@ -126,25 +128,25 @@ class RasterEstimator {
}
return true; // EMPTY Intersection
}
// world to grid transform
final AffineTransform2D w2G = (AffineTransform2D) reader.getOriginalGridToWorld(
PixelInCell.CELL_CORNER).inverse();
final Geometry rasterGeometry = JTS.transform(roiInNativeCRS_, w2G);
// try to make an estimate of the area we need to read
// NOTE I use the envelope since in the end I can only pass down
// a rectangular source region to the ImageIO-Ext reader, but in the end I am only going
// to read the tile I will need during processing as in this case I am going to perform
// deferred reads
areaRead = rasterGeometry.getEnvelope().getArea();
// TODO investigate on improved precision taking into account tiling on raster geometry
ReferencedEnvelope refEnvelope = JTS.toEnvelope(roiInNativeCRS_.getEnvelope());
scaling = new ScaleToTarget(reader, refEnvelope);
// TODO investigate on improved precision taking into account tiling on raster geometry
} else {
// No ROI, we are trying to read the entire coverage
final Rectangle2D originalGridRange = (GridEnvelope2D) reader.getOriginalGridRange();
areaRead = originalGridRange.getWidth() * originalGridRange.getHeight();
scaling = new ScaleToTarget(reader);
}
scaling.setTargetSize(targetSizeX, targetSizeY);
GridGeometry2D gg = scaling.getGridGeometry();
areaRead = gg.getGridRange2D().width * gg.getGridRange2D().height;
// checks on the area we want to download
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.fine("Area to read in pixels: " + areaRead);
......@@ -162,4 +164,6 @@ class RasterEstimator {
return true;
}
}
\ No newline at end of file
/* (c) 2015 Open Source Geospatial Foundation - all rights reserved
* This code is licensed under the GPL 2.0 license, available at the root
* application directory.
*/
package org.geoserver.wps.gs.download;
import java.awt.geom.AffineTransform;
import java.awt.geom.NoninvertibleTransformException;
import java.awt.image.RenderedImage;
import java.io.IOException;
import java.util.List;
import javax.media.jai.Interpolation;
import javax.media.jai.InterpolationNearest;
import javax.media.jai.JAI;
import javax.media.jai.Warp;
import javax.media.jai.WarpAffine;
import org.geoserver.data.util.CoverageUtils;
import org.geoserver.wcs.CoverageCleanerCallback;
import org.geotools.coverage.grid.GridCoverage2D;
import org.geotools.coverage.grid.GridEnvelope2D;
import org.geotools.coverage.grid.GridGeometry2D;
import org.geotools.coverage.grid.io.AbstractGridFormat;
import org.geotools.coverage.grid.io.GridCoverage2DReader;
import org.geotools.coverage.grid.io.OverviewPolicy;
import org.geotools.coverage.processing.CoverageProcessor;
import org.geotools.factory.GeoTools;
import org.geotools.factory.Hints;
import org.geotools.referencing.operation.builder.GridToEnvelopeMapper;
import org.geotools.referencing.operation.matrix.XAffineTransform;
import org.geotools.referencing.operation.transform.ProjectiveTransform;
import org.geotools.resources.coverage.CoverageUtilities;
import org.geotools.resources.image.ImageUtilities;
import org.jaitools.imageutils.ImageLayout2;
import org.opengis.coverage.processing.Operation;
import org.opengis.geometry.Envelope;
import org.opengis.parameter.GeneralParameterDescriptor;
import org.opengis.parameter.GeneralParameterValue;
import org.opengis.parameter.ParameterValueGroup;
import org.opengis.referencing.datum.PixelInCell;
import org.opengis.referencing.operation.MathTransform;
import org.opengis.referencing.operation.TransformException;
/**
* Class encapsulating the logic to scale a coverage to a pre-defined target size.
*
* @author Stefano Costa, GeoSolutions
*
*/
class ScaleToTarget {
/** The overview policy. By default, NEAREST policy is used **/
private OverviewPolicy overviewPolicy;
/** The interpolation method. By default, NEAREST interpolation is used **/
private Interpolation interpolation;
private GridCoverage2DReader reader;
private Envelope envelope;
private Integer adjustedTargetSizeX;
private Integer adjustedTargetSizeY;
/**
* Constructor.
*
* @param reader the coverage reader to use for reading metadata
*/
ScaleToTarget(GridCoverage2DReader reader) {
this(reader, null);
}
/**
* Two-args constructor.
*
* @param reader the coverage reader to use for reading metadata
* @param envelope the envelope of the ROI we want to scale (if <code>null</code>, the envelope of the whole coverage is used)
*/
ScaleToTarget(GridCoverage2DReader reader, Envelope envelope) {
checkNotNull(reader, "reader");
this.reader = reader;
this.envelope = envelope;
if (this.envelope == null) {
this.envelope = reader.getOriginalEnvelope();
}
this.interpolation = (Interpolation) ImageUtilities.NN_INTERPOLATION_HINT
.get(JAI.KEY_INTERPOLATION);
this.overviewPolicy = OverviewPolicy.NEAREST;
}
/**
* @return the interpolation
*/
public Interpolation getInterpolation() {
return interpolation;
}
/**
* @param interpolation the interpolation to set
*/
public void setInterpolation(Interpolation interpolation) {
checkNotNull(interpolation, "interpolation");
this.interpolation = interpolation;
}
/**
* @return the overviewPolicy
*/
public OverviewPolicy getOverviewPolicy() {
return overviewPolicy;
}
/**
* @param overviewPolicy the overviewPolicy to set
*/
public void setOverviewPolicy(OverviewPolicy overviewPolicy) {
checkNotNull(overviewPolicy, "overviewPolicy");
this.overviewPolicy = overviewPolicy;
}
/**
* @return the current target size
*/
public Integer[] getTargetSize() {
return new Integer[] { this.adjustedTargetSizeX, this.adjustedTargetSizeY };
}
/**
* Sets the size of the scaled image (target).
*
* <p>
* If one of the two inputs is omitted, the missing value is inferred from the provided one so that the aspect ratio of the specified envelope is
* preserved.
* </p>
*
* @param targetSizeX the size of the target image along the X axis
* @param targetSizeY the size of the target image along the Y axis
* @throws TransformException
*/
public void setTargetSize(Integer targetSizeX, Integer targetSizeY) throws TransformException {
// validate input
checkTargetSize(targetSizeX, "X");
checkTargetSize(targetSizeY, "Y");
// store input values in internal state
this.adjustedTargetSizeX = targetSizeX;
this.adjustedTargetSizeY = targetSizeY;
if (this.adjustedTargetSizeX == null && this.adjustedTargetSizeY == null) {
// no scaling should be done, return
return;
}
// adjust target size, if needed
if ((this.adjustedTargetSizeX == null && this.adjustedTargetSizeY != null)
|| (this.adjustedTargetSizeY == null && this.adjustedTargetSizeX != null)) {
// target size was specified for a single axis: calculate target size along the other
// axis preserving original aspect ratio
MathTransform g2w = reader.getOriginalGridToWorld(PixelInCell.CELL_CENTER);
GridGeometry2D gg = new GridGeometry2D(PixelInCell.CELL_CENTER, g2w, envelope,
GeoTools.getDefaultHints());
double width = gg.getGridRange2D().getWidth();
double height = gg.getGridRange2D().getHeight();
double whRatio = width / height;
if (this.adjustedTargetSizeY != null) {
// calculate X size
this.adjustedTargetSizeX = (int) Math.round(this.adjustedTargetSizeY * whRatio);
} else {
// calculate Y size
this.adjustedTargetSizeY = (int) Math.round(this.adjustedTargetSizeX / whRatio);
}
}
}
/**
* @return the grid geometry at the picked read resolution
* @throws IOException
*/
GridGeometry2D getGridGeometry() throws IOException {
MathTransform gridToCRS = getGridToCRSTransform();
GridGeometry2D gridGeometry = new GridGeometry2D(PixelInCell.CELL_CENTER, gridToCRS,
envelope, GeoTools.getDefaultHints());
return gridGeometry;
}