/* (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.wcs2_0; import java.awt.Rectangle; import java.awt.geom.AffineTransform; import java.awt.geom.NoninvertibleTransformException; import java.awt.image.RenderedImage; 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 net.opengis.wcs20.ScaleAxisByFactorType; import net.opengis.wcs20.ScaleAxisType; import net.opengis.wcs20.ScaleByFactorType; import net.opengis.wcs20.ScaleToExtentType; import net.opengis.wcs20.ScaleToSizeType; import net.opengis.wcs20.ScalingType; import net.opengis.wcs20.TargetAxisExtentType; import net.opengis.wcs20.TargetAxisSizeType; import org.eclipse.emf.common.util.EList; import org.geoserver.wcs.WCSInfo; import org.geoserver.wcs2_0.exception.WCS20Exception; import org.geotools.coverage.grid.GridCoverage2D; import org.geotools.coverage.grid.GridEnvelope2D; import org.geotools.coverage.grid.GridGeometry2D; import org.geotools.coverage.processing.CoverageProcessor; import org.geotools.coverage.processing.operation.Scale; import org.geotools.factory.Hints; import org.geotools.resources.coverage.CoverageUtilities; import org.geotools.util.Utilities; import it.geosolutions.jaiext.utilities.ImageLayout2; import org.opengis.coverage.grid.GridEnvelope; import org.opengis.coverage.processing.Operation; import org.opengis.parameter.ParameterValueGroup; import org.vfny.geoserver.util.WCSUtils; /** * {@link Enum} for implementing the management of the various scaling options available for the * scaling extension. * * <p> * This enum works as a factory to separate the code that handles the scaling operations. * * @author Simone Giannecchini, GeoSolutions * */ enum ScalingPolicy { DoNothing { @Override public GridCoverage2D scale(GridCoverage2D sourceGC, ScalingType scaling, Interpolation interpolation, Hints hints, WCSInfo wcsinfo) { Utilities.ensureNonNull("sourceGC", sourceGC); Utilities.ensureNonNull("ScalingType", scaling); Utilities.ensureNonNull("Interpolation", interpolation); return sourceGC; } }, /** * In this case we scale each axis by the same factor. * * <p> * We do rely on the {@link Scale} operation. */ ScaleByFactor { @Override public GridCoverage2D scale(GridCoverage2D sourceGC, ScalingType scaling, Interpolation interpolation, Hints hints, WCSInfo wcsinfo) { Utilities.ensureNonNull("sourceGC", sourceGC); Utilities.ensureNonNull("ScalingType", scaling); // get scale factor double[] scaleFactors = getScaleFactors(scaling); double scaleFactor = scaleFactors[0]; scaleFactor = arrangeScaleFactors(hints, new double[]{scaleFactor, scaleFactor})[0]; // checks if (scaleFactor <= 0) { throw new WCS20Exception("Invalid scale factor", WCS20Exception.WCS20ExceptionCode.InvalidScaleFactor, String.valueOf(scaleFactor)); } // return coverage unchanged if we don't scale if (scaleFactor == 1) { // NO SCALING 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(hints).doOperation( parameters, hints); } } // ==== check limits final GridGeometry2D gridGeometry = sourceGC.getGridGeometry(); final GridEnvelope gridRange = gridGeometry.getGridRange(); WCSUtils.checkOutputLimits(wcsinfo, new GridEnvelope2D(0, 0, (int) (gridRange.getSpan(gridGeometry.gridDimensionX) * scaleFactor), (int) (gridRange.getSpan(gridGeometry.gridDimensionY) * scaleFactor)), sourceGC.getRenderedImage().getSampleModel()); // === scale final Operation operation = CoverageProcessor.getInstance().getOperation("Scale"); final ParameterValueGroup parameters = operation.getParameters(); parameters.parameter("Source").setValue(sourceGC); parameters.parameter("interpolation").setValue( interpolation != null ? interpolation : InterpolationPolicy.getDefaultPolicy() .getInterpolation()); parameters.parameter("xScale").setValue(scaleFactor); parameters.parameter("yScale").setValue(scaleFactor); parameters.parameter("xTrans").setValue(0.0); parameters.parameter("yTrans").setValue(0.0); return (GridCoverage2D) CoverageProcessor.getInstance(hints).doOperation(parameters, hints); } }, /** * In this case we scale each axis bto a predefined size. * * <p> * We do rely on the {@link org.geotools.coverage.processing.operation.Warp} operation as the * final size must be respected on each axis. */ ScaleToSize { /** * In this case we must retain the lower bounds by scale the size, hence * {@link ScaleDescriptor} JAI operation cannot be used. Same goes for * {@link AffineDescriptor}, the only real option is {@link WarpDescriptor}. * * @param wcsinfo * */ @Override public GridCoverage2D scale(GridCoverage2D sourceGC, ScalingType scaling, Interpolation interpolation, Hints hints, WCSInfo wcsinfo) { // get scale size final int[] targetSize = getTargetSize(scaling); final int sizeX = targetSize[0]; final int sizeY = targetSize[1]; // scale final GridEnvelope2D sourceGE = sourceGC.getGridGeometry().getGridRange2D(); if (sizeY == sourceGE.width && sizeX == sourceGE.height) { // NO SCALING 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); } } // === enforce output limits WCSUtils.checkOutputLimits(wcsinfo, new GridEnvelope2D(0, 0, sizeX, sizeY), sourceGC .getRenderedImage().getSampleModel()); // create final warp final double scaleX = 1.0 * sizeX / sourceGE.width; final double scaleY = 1.0 * sizeY / 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, sizeX, sizeY); 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 != null ? interpolation : InterpolationPolicy.getDefaultPolicy() .getInterpolation()); parameters.parameter("backgroundValues").setValue( CoverageUtilities.getBackgroundValues(sourceGC));// TODO check and improve GridCoverage2D gc = (GridCoverage2D) CoverageProcessor.getInstance().doOperation( parameters, hints); return gc; } }, /** * In this case we scale each axis to a predefined extent. * * <p> * We do rely on the {@link org.geotools.coverage.processing.operation.Warp} operation as the * final extent must be respected on each axis. */ ScaleToExtent { @Override public GridCoverage2D scale(GridCoverage2D sourceGC, ScalingType scaling, Interpolation interpolation, Hints hints, WCSInfo wcsinfo) { Utilities.ensureNonNull("sourceGC", sourceGC); Utilities.ensureNonNull("ScalingType", scaling); // parse area final ScaleToExtentType scaleType = scaling.getScaleToExtent(); final EList<TargetAxisExtentType> targetAxisExtentElements = scaleType .getTargetAxisExtent(); TargetAxisExtentType xExtent = null, yExtent = null; for (TargetAxisExtentType axisExtentType : targetAxisExtentElements) { final String axisName = axisExtentType.getAxis(); if (axisName.equals("http://www.opengis.net/def/axis/OGC/1/i") || axisName.equals("i")) { xExtent = axisExtentType; } else if (axisName.equals("http://www.opengis.net/def/axis/OGC/1/j") || axisName.equals("j")) { yExtent = axisExtentType; } else { // TODO remove when supporting TIME and ELEVATION throw new WCS20Exception("Scale Axis Undefined", WCS20Exception.WCS20ExceptionCode.ScaleAxisUndefined, axisName); } } if (xExtent == null) { throw new WCS20Exception("Missing extent along i", WCS20Exception.WCS20ExceptionCode.InvalidExtent, "Null"); } if (yExtent == null) { throw new WCS20Exception("Missing extent along j", WCS20Exception.WCS20ExceptionCode.InvalidExtent, "Null"); } final int minx = (int) targetAxisExtentElements.get(0).getLow();// TODO should this be // int? final int maxx = (int) targetAxisExtentElements.get(0).getHigh(); final int miny = (int) targetAxisExtentElements.get(1).getLow(); final int maxy = (int) targetAxisExtentElements.get(1).getHigh(); // check on source geometry final GridEnvelope2D sourceGE = sourceGC.getGridGeometry().getGridRange2D(); if (minx >= maxx) { throw new WCS20Exception("Invalid Extent for dimension:" + targetAxisExtentElements.get(0).getAxis(), WCS20Exception.WCS20ExceptionCode.InvalidExtent, String.valueOf(maxx)); } if (miny >= maxy) { throw new WCS20Exception("Invalid Extent for dimension:" + targetAxisExtentElements.get(1).getAxis(), WCS20Exception.WCS20ExceptionCode.InvalidExtent, String.valueOf(maxy)); } final Rectangle destinationRectangle = new Rectangle(minx, miny, maxx - minx + 1, maxy - miny + 1); // UNSCALE if (destinationRectangle.equals(sourceGE)) { // NO SCALING 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(hints).doOperation( parameters, hints); } } // === enforce output limits WCSUtils.checkOutputLimits(wcsinfo, new GridEnvelope2D(destinationRectangle), sourceGC .getRenderedImage().getSampleModel()); // create final warp final double scaleX = 1.0 * destinationRectangle.width / sourceGE.width; final double scaleY = 1.0 * destinationRectangle.height / 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, destinationRectangle.x - scaleX * sourceMinX, // preserve sourceImage.getMinX() destinationRectangle.y - scaleY * sourceMinY); // preserve sourceImage.getMinY() // as per spec Warp warp; try { warp = new WarpAffine(affineTransform.createInverse()); } catch (NoninvertibleTransformException e) { throw new RuntimeException(e); } // impose size final ImageLayout2 layout = new ImageLayout2(destinationRectangle.x, destinationRectangle.y, destinationRectangle.width, destinationRectangle.height); 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 != null ? interpolation : InterpolationPolicy.getDefaultPolicy() .getInterpolation()); parameters.parameter("backgroundValues").setValue( CoverageUtilities.getBackgroundValues(sourceGC));// TODO check and improve GridCoverage2D gc = (GridCoverage2D) CoverageProcessor.getInstance().doOperation( parameters, hints); // RenderedImageBrowser.showChain(gc.getRenderedImage(),false); return gc; } }, /** * In this case we scale each axis by the a provided factor. * * <p> * We do rely on the {@link Scale} operation. */ ScaleAxesByFactor { @Override public GridCoverage2D scale(GridCoverage2D sourceGC, ScalingType scaling, Interpolation interpolation, Hints hints, WCSInfo wcsinfo) { Utilities.ensureNonNull("sourceGC", sourceGC); Utilities.ensureNonNull("ScalingType", scaling); // TODO dimension management // get scale factor double scaleFactors[] = getScaleFactors(scaling); double scaleFactorX = scaleFactors[0]; double scaleFactorY = scaleFactors[1]; scaleFactors = arrangeScaleFactors(hints, scaleFactors); // unscale if (scaleFactorX == 1.0 && scaleFactorY == 1.0) { // NO SCALING 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(hints).doOperation( parameters, hints); } } // ==== check limits final GridGeometry2D gridGeometry = sourceGC.getGridGeometry(); final GridEnvelope gridRange = gridGeometry.getGridRange(); WCSUtils.checkOutputLimits(wcsinfo, new GridEnvelope2D(0, 0, (int) (gridRange.getSpan(gridGeometry.gridDimensionX) * scaleFactorX), (int) (gridRange.getSpan(gridGeometry.gridDimensionY) * scaleFactorY)), sourceGC.getRenderedImage().getSampleModel()); // scale final Operation operation = CoverageProcessor.getInstance().getOperation("Scale"); final ParameterValueGroup parameters = operation.getParameters(); parameters.parameter("Source").setValue(sourceGC); parameters.parameter("interpolation").setValue( interpolation != null ? interpolation : InterpolationPolicy.getDefaultPolicy() .getInterpolation()); parameters.parameter("xScale").setValue(scaleFactors[0]); parameters.parameter("yScale").setValue(scaleFactors[1]); parameters.parameter("xTrans").setValue(0.0); parameters.parameter("yTrans").setValue(0.0); return (GridCoverage2D) CoverageProcessor.getInstance(hints).doOperation(parameters, hints); } }; /** * Scale the provided {@link GridCoverage2D} according to the provided {@link ScalingType} and * the provided {@link Interpolation} and {@link Hints}. * * @param sourceGC the {@link GridCoverage2D} to scale. * @param scaling the instance of {@link ScalingType} that contains he type of scaling to * perform. * @param interpolation the {@link Interpolation} to use. In case it is <code>null</code> we * will use the {@link InterpolationPolicy} default value. * @param hints {@link Hints} to use during this operation. * @param wcsinfo the current instance of {@link WCSInfo} that contains wcs config for GeoServer * @return a scaled version of the input {@link GridCoverage2D}. It cam be subsampled or * oversampled, it depends on the {@link ScalingType} content. */ abstract public GridCoverage2D scale(GridCoverage2D sourceGC, ScalingType scaling, Interpolation interpolation, Hints hints, WCSInfo wcsinfo); /** * Retrieve the {@link ScalingPolicy} from the provided {@link ScalingType} * @param scaling * */ public static ScalingPolicy getPolicy(ScalingType scaling) { if (scaling != null) { if (scaling.getScaleAxesByFactor() != null) { return ScaleAxesByFactor; } if (scaling.getScaleByFactor() != null) { return ScaleByFactor; } if (scaling.getScaleToExtent() != null) { return ScaleToExtent; } if (scaling.getScaleToSize() != null) { return ScaleToSize; } } return DoNothing; } /** * Extract the requested targetSize from this scaling extension in case the * provided scaling is a ScaleToSizeType type. * * Throw an {@link IllegalArgumentException} in case the scaling type is not a * supported one. * * @param scaling * */ public static int[] getTargetSize(ScalingType scaling) { if (scaling.getScaleToSize() != null) { final ScaleToSizeType scaleType = scaling.getScaleToSize(); final EList<TargetAxisSizeType> targetAxisSizeElements = scaleType.getTargetAxisSize(); TargetAxisSizeType xSize = null, ySize = null; for (TargetAxisSizeType axisSizeType : targetAxisSizeElements) { final String axisName = axisSizeType.getAxis(); if (axisName.equals("http://www.opengis.net/def/axis/OGC/1/i") || axisName.equals("i")) { xSize = axisSizeType; } else if (axisName.equals("http://www.opengis.net/def/axis/OGC/1/j") || axisName.equals("j")) { ySize = axisSizeType; } else { // TODO remove when supporting TIME and ELEVATION throw new WCS20Exception("Scale Axis Undefined", WCS20Exception.WCS20ExceptionCode.ScaleAxisUndefined, axisName); } } final int sizeX = (int) xSize.getTargetSize();// TODO should this be int? if (sizeX <= 0) { throw new WCS20Exception("Invalid target size", WCS20Exception.WCS20ExceptionCode.InvalidExtent, Integer.toString(sizeX)); } final int sizeY = (int) ySize.getTargetSize();// TODO should this be int? if (sizeY <= 0) { throw new WCS20Exception("Invalid target size", WCS20Exception.WCS20ExceptionCode.InvalidExtent, Integer.toString(sizeY)); } return new int[] { sizeX, sizeY }; } else if (scaling.getScaleToExtent() != null) { ScaleToExtentType ste = scaling.getScaleToExtent(); TargetAxisExtentType xSize = null, ySize = null; for (TargetAxisExtentType axisSizeType : ste.getTargetAxisExtent()) { final String axisName = axisSizeType.getAxis(); if (axisName.equals("http://www.opengis.net/def/axis/OGC/1/i") || axisName.equals("i")) { xSize = axisSizeType; } else if (axisName.equals("http://www.opengis.net/def/axis/OGC/1/j") || axisName.equals("j")) { ySize = axisSizeType; } else { // TODO remove when supporting TIME and ELEVATION throw new WCS20Exception("Scale Axis Undefined", WCS20Exception.WCS20ExceptionCode.ScaleAxisUndefined, axisName); } } final int sizeX = (int) (xSize.getHigh() - xSize.getLow());// TODO should this be int? if (sizeX <= 0) { throw new WCS20Exception("Invalid target extent, high is greater than low", WCS20Exception.WCS20ExceptionCode.InvalidExtent, Integer.toString((int) xSize.getHigh())); } final int sizeY = (int) (ySize.getHigh() - ySize.getLow()); if (sizeY <= 0) { throw new WCS20Exception("Invalid target extent, high is greater than low", WCS20Exception.WCS20ExceptionCode.InvalidExtent, Integer.toString((int) ySize.getHigh())); } return new int[] { sizeX, sizeY }; } else { throw new IllegalArgumentException("targe size can not be computed from this type of scaling: " + getPolicy(scaling)); } } /** * Extract the requested scaleFactors from this scaling extension in case the * provided scaling is a ScaleXXXFactor type. * * Throw an {@link IllegalArgumentException} in case the scaling type is not a * supported one. * * @param scaling * */ public static double[] getScaleFactors(ScalingType scaling) { ScalingPolicy policy = getPolicy(scaling); switch (policy) { case ScaleByFactor: final ScaleByFactorType scaleByFactorType = scaling.getScaleByFactor(); double scaleFactor = scaleByFactorType.getScaleFactor(); return new double[] { scaleFactor, scaleFactor }; case ScaleAxesByFactor: final ScaleAxisByFactorType scaleType = scaling.getScaleAxesByFactor(); final EList<ScaleAxisType> targetAxisScaleElements = scaleType.getScaleAxis(); ScaleAxisType xScale = null, yScale = null; for (ScaleAxisType scaleAxisType : targetAxisScaleElements) { final String axisName = scaleAxisType.getAxis(); if (axisName.equals("http://www.opengis.net/def/axis/OGC/1/i") || axisName.equals("i")) { xScale = scaleAxisType; } else if (axisName.equals("http://www.opengis.net/def/axis/OGC/1/j") || axisName.equals("j")) { yScale = scaleAxisType; } else { // TODO remove when supporting TIME and ELEVATION throw new WCS20Exception("Scale Axis Undefined", WCS20Exception.WCS20ExceptionCode.ScaleAxisUndefined, axisName); } } if (xScale == null) { throw new WCS20Exception("Missing scale factor along i", WCS20Exception.WCS20ExceptionCode.InvalidScaleFactor, "Null"); } if (yScale == null) { throw new WCS20Exception("Missing scale factor along j", WCS20Exception.WCS20ExceptionCode.InvalidScaleFactor, "Null"); } final double scaleFactorX = xScale.getScaleFactor(); if (scaleFactorX <= 0) { throw new WCS20Exception("Invalid scale factor", WCS20Exception.WCS20ExceptionCode.InvalidScaleFactor, Double.toString(scaleFactorX)); } final double scaleFactorY = yScale.getScaleFactor(); if (scaleFactorY <= 0) { throw new WCS20Exception("Invalid scale factor", WCS20Exception.WCS20ExceptionCode.InvalidScaleFactor, Double.toString(scaleFactorY)); } return new double[] { scaleFactorX, scaleFactorY }; default: throw new IllegalArgumentException( "scale factors can not be computed from this type of scaling: " + policy); } } /** * In case some scaling factor have been pre-applied, make sure to arrange * the requested target scaleFactors by taking into account the previous ones. * * This is usually required when using overviews. Suppose you want to get a * target scaleFactor of 0.00001 and the worst overview provide you a scale factor * of 0.0001, then the current scaleFactor need to be adjusted by a remaining 0.1 * factor. * * @param hints * @param scaleFactors * @return the arranged scaleFactor */ private static double[] arrangeScaleFactors(Hints hints, final double[] scaleFactors) { if (hints != null && hints.containsKey(GetCoverage.PRE_APPLIED_SCALE)) { Double[] preAppliedScale = (Double[]) hints.get(GetCoverage.PRE_APPLIED_SCALE); if (preAppliedScale != null) { scaleFactors[0] = scaleFactors[0] * preAppliedScale[0]; scaleFactors[1] = scaleFactors[1] * preAppliedScale[1]; } } return scaleFactors; } }