/* (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 it.geosolutions.jaiext.utilities.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; } /** * Reads the coverage using the provided reader and read parameters, and then scales it to the set target size. * * <p> * The method properly sets the {@link AbstractGridFormat#READ_GRIDGEOMETRY2D} parameter before reading. * </p> * * <p> * If no target size is set, or the requested resolution matches the native resolution of the image, or the resolution of one of its overviews, * scaling is not performed. * </p> * * <p> * In any case, if the selected interpolation method is not Nearest Neighbor, interpolation is performed. * </p> * * @param readParameters the read parameters to pass to the coverage reader * @return the scaled coverage * @throws IOException */ public GridCoverage2D scale(GeneralParameterValue[] readParameters) throws IOException { if (readParameters == null) { readParameters = new GeneralParameterValue[] {}; } // setup reader parameters to have it exploit overviews final ParameterValueGroup readParametersDescriptor = reader.getFormat().getReadParameters(); final List<GeneralParameterDescriptor> parameterDescriptors = readParametersDescriptor .getDescriptor().descriptors(); readParameters = CoverageUtils.mergeParameter(parameterDescriptors, readParameters, getGridGeometry(), AbstractGridFormat.READ_GRIDGEOMETRY2D.getName().getCode()); GridCoverage2D inputGC = reader.read(readParameters); return scale(inputGC); } /** * Scale the provided coverage to the set target size. * * <p> * Please note that the method assumes the coverage was read taking overviews into account, i.e. by properly setting the * {@link AbstractGridFormat#READ_GRIDGEOMETRY2D} parameter. * </p> * * <p> * If no target size was set, or the requested resolution matches the native resolution of the image, or the resolution of one of its overviews, * scaling is not performed. * </p> * * <p> * In any case, if the selected interpolation method is not Nearest Neighbor, interpolation is performed. * </p> * * @param sourceGC the scaled coverage * * @throws IOException */ /* * Code adapted from org.geoserver.wcs2_0.ScalingPolicy.ScaleToSize */ public GridCoverage2D scale(GridCoverage2D sourceGC) throws IOException { checkNotNull(sourceGC, "sourceGC)"); if (!isTargetSizeSet() && (interpolation instanceof InterpolationNearest)) { return sourceGC; } // scale final Hints hints = GeoTools.getDefaultHints(); final GridEnvelope2D sourceGE = getGridGeometry().getGridRange2D(); if ((isTargetSizeSet() && this.adjustedTargetSizeX.equals(sourceGE.width) && this.adjustedTargetSizeY == sourceGE.height) || (!isTargetSizeSet())) { // NO NEED TO SCALE, do we need interpolation? if (interpolation instanceof InterpolationNearest) { return sourceGC; } else { // interpolate coverage if requested and not nearest!!!! final Operation operation = CoverageProcessor.getInstance().getOperation("Warp"); final ParameterValueGroup parameters = operation.getParameters(); parameters.parameter("Source").setValue(sourceGC); parameters.parameter("warp").setValue( new WarpAffine(AffineTransform.getScaleInstance(1, 1)));// identity parameters.parameter("interpolation").setValue(interpolation); parameters.parameter("backgroundValues").setValue( CoverageUtilities.getBackgroundValues(sourceGC));// TODO check and // improve return (GridCoverage2D) CoverageProcessor.getInstance().doOperation(parameters, hints); } } // create final warp final double scaleX = 1.0 * this.adjustedTargetSizeX / sourceGE.width; final double scaleY = 1.0 * this.adjustedTargetSizeY / sourceGE.height; final RenderedImage sourceImage = sourceGC.getRenderedImage(); final int sourceMinX = sourceImage.getMinX(); final int sourceMinY = sourceImage.getMinY(); final AffineTransform affineTransform = new AffineTransform(scaleX, 0, 0, scaleY, sourceMinX - scaleX * sourceMinX, // preserve sourceImage.getMinX() sourceMinY - scaleY * sourceMinY); // preserve sourceImage.getMinY() as per spec Warp warp; try { warp = new WarpAffine(affineTransform.createInverse()); } catch (NoninvertibleTransformException e) { throw new RuntimeException(e); } // impose final final ImageLayout2 layout = new ImageLayout2(sourceMinX, sourceMinY, this.adjustedTargetSizeX, this.adjustedTargetSizeY); hints.add(new Hints(JAI.KEY_IMAGE_LAYOUT, layout)); final Operation operation = CoverageProcessor.getInstance().getOperation("Warp"); final ParameterValueGroup parameters = operation.getParameters(); parameters.parameter("Source").setValue(sourceGC); parameters.parameter("warp").setValue(warp); parameters.parameter("interpolation").setValue(interpolation); parameters.parameter("backgroundValues").setValue( CoverageUtilities.getBackgroundValues(sourceGC));// TODO check and improve GridCoverage2D gc = (GridCoverage2D) CoverageProcessor.getInstance().doOperation( parameters, hints); return gc; } /** * Computes the transformation between raster and world coordinates, taking scaling into account. * * @return the grid-to-CRS transformation * @throws IOException */ MathTransform getGridToCRSTransform() throws IOException { // scaling transform AffineTransform scaleTransform = getScaleTransform(); // grid-to-world transformation AffineTransform g2w = (AffineTransform) reader .getOriginalGridToWorld(PixelInCell.CELL_CENTER); // final transformation: g2w + scaling AffineTransform finalTransform = new AffineTransform(g2w); finalTransform.concatenate(scaleTransform); return ProjectiveTransform.create(finalTransform); } /** * Computes the scaling transformation for the overview which would be picked for the requested resolution. * * @return the scaling transformation * @throws IOException */ private AffineTransform getScaleTransform() throws IOException { // getting the native resolution final double[] nativeResolution = computeNativeResolution(); // getting the requested resolution final double[] requestedResolution = computeRequestedResolution(); // getting the read resolution from the reader, based on the current Overview Policy final double[] readResolution = computeReadingResolution(requestedResolution); // setup a scaling to get the transformation to be used to access the specified overview AffineTransform scaleTransform = new AffineTransform(); double[] scaleFactors = new double[] { readResolution[0] / nativeResolution[0], readResolution[1] / nativeResolution[1] }; scaleTransform.scale(scaleFactors[0], scaleFactors[1]); return scaleTransform; } /** * @return the native resolution */ double[] computeNativeResolution() { double[] nativeResolution = new double[2]; MathTransform transform = reader.getOriginalGridToWorld(PixelInCell.CELL_CENTER); AffineTransform af = (AffineTransform) transform; // getting the native resolution nativeResolution[0] = XAffineTransform.getScaleX0(af); nativeResolution[1] = XAffineTransform.getScaleY0(af); return nativeResolution; } /** * @return a resolution satisfying the scaling */ double[] computeRequestedResolution() { if (!isTargetSizeSet()) { return computeNativeResolution(); } double[] requestedResolution = new double[2]; // Getting the requested resolution (using envelope and requested scaleSize) final GridToEnvelopeMapper mapper = new GridToEnvelopeMapper(new GridEnvelope2D(0, 0, this.adjustedTargetSizeX, this.adjustedTargetSizeY), this.envelope); AffineTransform scalingTransform = mapper.createAffineTransform(); requestedResolution[0] = XAffineTransform.getScaleX0(scalingTransform); requestedResolution[1] = XAffineTransform.getScaleY0(scalingTransform); return requestedResolution; } /** * @param requestedResolution the requested resolution * @return the resolution of the overview which would be picked out for the provided requested resolution using the current OverviewPolicy * @throws IOException */ double[] computeReadingResolution(double[] requestedResolution) throws IOException { return reader.getReadingResolutions(overviewPolicy, requestedResolution); } private boolean isTargetSizeSet() { return this.adjustedTargetSizeX != null || this.adjustedTargetSizeY != null; } private void checkNotNull(Object param, String paramName) { if (param == null) { throw new IllegalArgumentException(paramName + " parameter must be specified"); } } private void checkTargetSize(Integer value, String dim) { if (value != null && value <= 0) { throw new IllegalArgumentException(dim + " target size must be > 0"); } } }