/* * GeoTools - The Open Source Java GIS Toolkit * http://geotools.org * * (C) 2005-2008, Open Source Geospatial Foundation (OSGeo) * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; * version 2.1 of the License. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. */ package org.geotools.referencing.operation.transform; import java.awt.geom.Point2D; import java.awt.geom.Rectangle2D; import java.io.Serializable; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Calendar; import java.util.Date; import java.util.GregorianCalendar; import java.util.TimeZone; import javax.media.jai.JAI; import javax.media.jai.Warp; import javax.media.jai.WarpAffine; import javax.media.jai.WarpCubic; import javax.media.jai.WarpGeneralPolynomial; import javax.media.jai.WarpPolynomial; import javax.media.jai.WarpQuadratic; import org.geotools.metadata.iso.citation.Citations; import org.geotools.parameter.DefaultParameterDescriptor; import org.geotools.parameter.Parameter; import org.geotools.parameter.ParameterGroup; import org.geotools.referencing.NamedIdentifier; import org.geotools.referencing.operation.MathTransformProvider; import org.geotools.resources.XArray; import org.geotools.resources.i18n.Vocabulary; import org.geotools.resources.i18n.VocabularyKeys; import org.geotools.util.Utilities; import org.opengis.parameter.ParameterDescriptor; import org.opengis.parameter.ParameterDescriptorGroup; import org.opengis.parameter.ParameterNotFoundException; import org.opengis.parameter.ParameterValue; import org.opengis.parameter.ParameterValueGroup; import org.opengis.referencing.operation.MathTransform; import org.opengis.referencing.operation.MathTransform2D; import org.opengis.referencing.operation.NoninvertibleTransformException; import org.opengis.referencing.operation.Transformation; /** * Wraps an arbitrary {@link Warp} object as a {@linkplain MathTransform2D two-dimensional transform}. * Calls to {@linkplain #transform(float[],int,float[],int,int) transform} methods are forwarded to * the {@link Warp#warpPoint(int,int,float[]) warpPoint} method, or something equivalent. This * implies that source coordinates may be rounded to nearest integers before the transformation * is applied. * <p> * This transform is typically used with {@linkplain org.geotools.coverage.processing.operation.Resample * grid coverage "Resample" operation} for reprojecting an image. Source and destination coordinates * are usually pixel coordinates in source and target image, which is why this transform may use * integer arithmetic. * <p> * This math transform can be created alone (by invoking its public constructors directly), or it * can be created by a factory like {@link LocalizationGrid}. * <p> * For more information on image warp, see * <A HREF="http://java.sun.com/products/java-media/jai/forDevelopers/jai1_0_1guide-unc/Geom-image-manip.doc.html">Geometric * Image Manipulation</A> in the <cite>Programming in Java Advanced Imaging</cite> guide. * * @since 2.1 * * @source $URL$ * @version $Id$ * @author Martin Desruisseaux * @author Alessio Fabiani * * @see LocalizationGrid#getPolynomialTransform(int) * @see Warp * @see javax.media.jai.WarpOpImage * @see javax.media.jai.operator.WarpDescriptor */ public class WarpTransform2D extends AbstractMathTransform implements MathTransform2D, Serializable { /** * Serial number for interoperability with different versions. */ private static final long serialVersionUID = -7949539694656719923L; private final static boolean USE_HACK; static{ final String buildVersion=JAI.getBuildVersion(); final SimpleDateFormat df= new SimpleDateFormat("yyyy-MM-dd' 'hh:mm:ss.SSSZ"); final TimeZone tz= TimeZone.getTimeZone("UTC"); df.setTimeZone(tz); boolean hack=false; try { final Date date_ = buildVersion!=null?df.parse(buildVersion):new java.util.Date(); //reduce granularity to minute final GregorianCalendar tempCal = new GregorianCalendar(tz); tempCal.setTime(date_); tempCal.set(Calendar.HOUR_OF_DAY, 0); tempCal.set(Calendar.MINUTE, 0); tempCal.set(Calendar.SECOND, 0); tempCal.set(Calendar.MILLISECOND, 0); final Date date=tempCal.getTime(); // 113 version final GregorianCalendar version113= new GregorianCalendar(tz); version113.set(2006, 8, 12,0,0,0); version113.set(Calendar.MILLISECOND, 0); // we need the hacke only if we are using 1.1.3 hack=!date.after(version113.getTime()); } catch (ParseException e) { hack=false; } USE_HACK=hack; } /** * The maximal polynomial degree allowed. * * @since 2.4 */ public static final int MAX_DEGREE = 7; /** * The warp object. Transformations will be applied using the * {@link Warp#warpPoint(int,int,float[]) warpPoint} method or something equivalent. */ private final Warp warp; /** * The inverse math transform. */ private final WarpTransform2D inverse; /** * Constructs a warp transform that approximatively maps the given source coordinates to the * given destination coordinates. The transformation is performed using some polynomial warp * with the degree supplied in argument. The number of points required for each degree of warp * are as follows: * <p> * <table> * <tr><th>Degree of Warp</th><th>Number of Points</th></tr> * <tr><td>1</td><td>3</td></tr> * <tr><td>2</td><td>6</td></tr> * <tr><td>3</td><td>10</td></tr> * <tr><td>4</td><td>15</td></tr> * <tr><td>5</td><td>21</td></tr> * <tr><td>6</td><td>28</td></tr> * <tr><td>7</td><td>36</td></tr> * </table> * * @param srcCoords Source coordinates. * @param dstCoords Desination coordinates. * @param degree The desired degree of the warp polynomials. */ public WarpTransform2D(final Point2D[] srcCoords, final Point2D[] dstCoords, final int degree) { this(null, srcCoords, 0, null, dstCoords, 0, Math.min(srcCoords.length, dstCoords.length), degree); } /** * Constructs a warp transform that approximatively maps the given source coordinates to the * given destination coordinates. The transformation is performed using some polynomial warp * with the degree supplied in argument. * * @param srcBounds Bounding box of source coordinates, or {@code null} if unknow. * @param srcCoords Source coordinates. * @param srcOffset The inital entry of {@code srcCoords} to be used. * @param dstBounds Bounding box of destination coordinates, or {@code null} if unknow. * @param dstCoords Destination coordinates. * @param dstOffset The inital entry of {@code destCoords} to be used. * @param numCoords The number of coordinates from {@code srcCoords} and {@code destCoords} * to be used. * @param degree The desired degree of the warp polynomials. */ public WarpTransform2D(final Rectangle2D srcBounds, final Point2D[] srcCoords, final int srcOffset, final Rectangle2D dstBounds, final Point2D[] dstCoords, final int dstOffset, final int numCoords, final int degree) { this(srcBounds, toFloat(srcCoords, srcOffset, numCoords), 0, dstBounds, toFloat(dstCoords, dstOffset, numCoords), 0, numCoords, degree, false); } /** * Converts an array of points into an array of floats. * This is used internally for the above constructor only. */ private static float[] toFloat(final Point2D[] points, int offset, final int numCoords) { final float[] array = new float[numCoords * 2]; for (int i=0; i<array.length;) { final Point2D point = points[offset++]; array[i++] = (float) point.getX(); array[i++] = (float) point.getY(); } return array; } /** * Constructs a warp transform that approximatively maps the given source coordinates to the * given destination coordinates. The transformation is performed using some polynomial warp * with the degree supplied in argument. * * @param srcBounds Bounding box of source coordinates, or {@code null} if unknow. * @param srcCoords Source coordinates with <var>x</var> and <var>y</var> alternating. * @param srcOffset The inital entry of {@code srcCoords} to be used. * @param dstBounds Bounding box of destination coordinates, or {@code null} if unknow. * @param dstCoords Destination coordinates with <var>x</var> and <var>y</var> alternating. * @param dstOffset The inital entry of {@code destCoords} to be used. * @param numCoords The number of coordinates from {@code srcCoords} and {@code destCoords} * to be used. * @param degree The desired degree of the warp polynomials. */ public WarpTransform2D(final Rectangle2D srcBounds, final float[] srcCoords, final int srcOffset, final Rectangle2D dstBounds, final float[] dstCoords, final int dstOffset, final int numCoords, final int degree) { this(srcBounds, srcCoords, srcOffset, dstBounds, dstCoords, dstOffset, numCoords, degree, true); } /** * Work around for a bug in WarpPolynomial.createWarp(...). This constructor should move in * the one above when the {@code cloneCoords} argument will no longer be needed (after the * JAI bug get fixed). */ private WarpTransform2D(final Rectangle2D srcBounds, float[] srcCoords, int srcOffset, final Rectangle2D dstBounds, float[] dstCoords, int dstOffset, final int numCoords, final int degree, boolean cloneCoords) { final float preScaleX, preScaleY, postScaleX, postScaleY; if (srcBounds != null) { preScaleX = (float) srcBounds.getWidth(); preScaleY = (float) srcBounds.getHeight(); } else { preScaleX = getWidth(srcCoords, srcOffset , numCoords); preScaleY = getWidth(srcCoords, srcOffset+1, numCoords); } if (dstBounds != null) { postScaleX = (float) dstBounds.getWidth(); postScaleY = (float) dstBounds.getHeight(); } else { postScaleX = getWidth(dstCoords, dstOffset , numCoords); postScaleY = getWidth(dstCoords, dstOffset+1, numCoords); } /* * Workaround for a bug in WarpPolynomial.create(...): the later scale coordinates * according the scale values, but the 'preScale' and 'postScale' are interchanged. * When JAI bug will be fixed, delete all the following block until the next comment. */ if(USE_HACK){ final double scaleX = preScaleX / postScaleX; final double scaleY = preScaleY / postScaleY; if (scaleX!=1 || scaleY!=1) { final int n = numCoords*2; if (cloneCoords) { float[] o; o=srcCoords; srcCoords=new float[n]; System.arraycopy(o, srcOffset, srcCoords, 0, n); srcOffset=0; o=dstCoords; dstCoords=new float[n]; System.arraycopy(o, dstOffset, dstCoords, 0, n); dstOffset=0; } for (int i=0; i<n;) { srcCoords[srcOffset + i ] /= scaleX; dstCoords[dstOffset + i++] *= scaleX; srcCoords[srcOffset + i ] /= scaleY; dstCoords[dstOffset + i++] *= scaleY; } } } /* * Note: Warp semantic (transforms coordinates from destination to source) is the * opposite of MathTransform semantic (transforms coordinates from source to * destination). We have to interchange source and destination arrays for the * direct transform. */ warp = WarpPolynomial.createWarp(dstCoords, dstOffset, srcCoords, srcOffset, numCoords, 1/preScaleX, 1/preScaleY, postScaleX, postScaleY, degree); inverse = new WarpTransform2D( WarpPolynomial.createWarp(srcCoords, srcOffset, dstCoords, dstOffset, numCoords, 1/postScaleX, 1/postScaleY, preScaleX, preScaleY, degree), this); } /** * Returns the maximum minus the minimum ordinate int the specifie array. * This is used internally for the above constructor only. */ private static float getWidth(final float[] array, int offset, int num) { float min = Float.POSITIVE_INFINITY; float max = Float.NEGATIVE_INFINITY; while (--num >= 0) { float value = array[offset]; if (value < min) min = value; if (value > max) max = value; offset += 2; } return max - min; } /** * Constructs a transform using the specified warp object. Transformations will be applied * using the {@link Warp#warpPoint(int,int,float[]) warpPoint} method or something equivalent. * * @param warp The image warp to wrap into a math transform. * @param inverse An image warp to uses for the {@linkplain #inverse inverse transform}, * or {@code null} in none. */ protected WarpTransform2D(final Warp warp, final Warp inverse) { ensureNonNull("warp", warp); this.warp = warp; this.inverse = (inverse!=null) ? new WarpTransform2D(inverse, this) : null; } /** * Constructs a transform using the specified warp object. This private constructor is used * for the construction of {@link #inverse} transform only. */ private WarpTransform2D(final Warp warp, final WarpTransform2D inverse) { this.warp = warp; this.inverse = inverse; } /** * Returns a transform using the specified warp object. Transformations will be applied * using the {@link Warp#warpPoint(int,int,float[]) warpPoint} method or something equivalent. * * @param warp The image warp to wrap into a math transform. * @return The transform for the given warp. */ public static MathTransform2D create(final Warp warp) { if (warp instanceof WarpAdapter) { return ((WarpAdapter) warp).getTransform(); } return new WarpTransform2D(warp, (Warp)null); } /** * Returns a {@linkplain Warp image warp} for the specified transform. The * {@link Warp#warpPoint(int,int,float[]) Warp.warpPoint} method transforms coordinates from * source to target CRS. Note that JAI's {@linkplain javax.media.jai.operator.WarpDescriptor * warp operation} needs a warp object with the opposite semantic (i.e. the image warp must * transforms coordinates from target to source CRS). Consequently, consider invoking * {@code getWarp(transform.inverse())} if the warp object is going to be used in an * image reprojection. * * @param name The image or {@linkplain org.geotools.coverage.grid.GridCoverage2D coverage} * name, or {@code null} in unknow. Used only for formatting error message if some * {@link org.opengis.referencing.operation.TransformException} are thrown by the * supplied transform. * @param transform The transform to returns as an image warp. * @return The warp for the given transform. * * @todo We should check for {@link ConcatenatedTransform}. If we detect that a * {@code WarpTransform2D} is concatenated with {@code AffineTransform} only, and if the * {@code AffineTransform} has scale factors only, then we can ommit the {@code AffineTransform} * and merge the scale factors with {@link WarpPolynomial} preScaleX, preScaleY, postScaleX and * postScaleY. Additionnaly, the translation term for the post-AffineTransform may also be * merged with the first coefficients of WarpPolynomial.xCoeffs and yCoeffs. See GEOT-521. * * @todo Move this method in some other factory class. Use the optimization applied by * Resampler2D in the case of AffineTransform, and remove that optimization from * Resampler2D. */ public static Warp getWarp(CharSequence name, final MathTransform2D transform) { if (transform instanceof WarpTransform2D) { return ((WarpTransform2D) transform).getWarp(); } if (name == null) { name = Vocabulary.formatInternational(VocabularyKeys.UNKNOWN); } return new WarpAdapter(name, transform); } /** * Returns {@linkplain Warp image warp} wrapped by this transform. The * {@link Warp#warpPoint(int,int,float[]) Warp.warpPoint} method transforms coordinates from * source to target CRS. Note that JAI's {@linkplain javax.media.jai.operator.WarpDescriptor * warp operation} needs a warp object with the opposite semantic (i.e. the image warp must * transforms coordinates from target to source CRS). Consequently, consider invoking * <code>{@linkplain #inverse}.getWarp()</code> if the warp object is going to be used in an * image reprojection. */ public Warp getWarp() { return warp; } /** * Returns the parameter descriptors for this math transform. */ @Override public ParameterDescriptorGroup getParameterDescriptors() { if (warp instanceof WarpPolynomial) { return Provider.PARAMETERS; } else { return super.getParameterDescriptors(); } } /** * Returns the parameter values for this math transform. */ @Override public ParameterValueGroup getParameterValues() { if (warp instanceof WarpPolynomial) { final WarpPolynomial poly = (WarpPolynomial) warp; final ParameterValue[] p = new ParameterValue[7]; int c = 0; p[c++] = new Parameter(Provider.DEGREE, Integer.valueOf(poly.getDegree())); p[c++] = new Parameter(Provider.X_COEFFS, poly.getXCoeffs()); p[c++] = new Parameter(Provider.Y_COEFFS, poly.getYCoeffs()); float s; if ((s=poly.getPreScaleX ()) != 1) p[c++] = new Parameter(Provider. PRE_SCALE_X, s); if ((s=poly.getPreScaleY ()) != 1) p[c++] = new Parameter(Provider. PRE_SCALE_Y, s); if ((s=poly.getPostScaleX()) != 1) p[c++] = new Parameter(Provider.POST_SCALE_X, s); if ((s=poly.getPostScaleY()) != 1) p[c++] = new Parameter(Provider.POST_SCALE_Y, s); return new ParameterGroup(getParameterDescriptors(), XArray.resize(p, c)); } else { return super.getParameterValues(); } } /** * Returns the dimension of input points. */ public int getSourceDimensions() { return 2; } /** * Returns the dimension of output points. */ public int getTargetDimensions() { return 2; } /** * Tests if this transform is the identity transform. */ @Override public boolean isIdentity() { return false; } /** * Transforms source coordinates (usually pixel indices) into destination coordinates * (usually "real world" coordinates). * * @param ptSrc the specified coordinate point to be transformed. * @param ptDst the specified coordinate point that stores the result of transforming * {@code ptSrc}, or {@code null}. * @return the coordinate point after transforming {@code ptSrc} and storing the result in * {@code ptDst}. */ @Override public Point2D transform(Point2D ptSrc, Point2D ptDst) { /* * We have to copy the coordinate in a temporary point object because we don't know * neither the ptSrc or ptDst type. Since mapDestPoint returns a clone of the point * given in argument, giving ptSrc directly would result in a lost of precision if * its type is java.awt.Point (for example), even if ptDst had a greater precision. * * There is also an other reason for creating a temporary object: * JAI's Warp is designed for mapping pixel coordinates in J2SE's image. In JAI, pixel * coordinates map by definition to the pixel's upper left corner. But for interpolation * purpose, JAI needs to map pixel's center. This introduce a shift of 0.5, which is * documented (for example) in WarpAffine.mapDestPoint(Point2D). */ ptSrc = new PointDouble(ptSrc.getX() - 0.5, ptSrc.getY() - 0.5); final Point2D result = warp.mapDestPoint(ptSrc); result.setLocation(result.getX() + 0.5, result.getY() + 0.5); if (ptDst == null) { // Do not returns 'result' directly, since it has tricked 'clone()' method. ptDst = new Point2D.Float(); } ptDst.setLocation(result); return ptDst; } /** * Transforms source coordinates (usually pixel indices) into destination coordinates * (usually "real world" coordinates). */ @Override public void transform(final float[] srcPts, int srcOff, final float[] dstPts, int dstOff, int numPts) { final int postIncrement; if (srcPts == dstPts && srcOff < dstOff) { srcOff += (numPts-1)*2; dstOff += (numPts-1)*2; postIncrement = -4; } else { postIncrement = 0; } final Point2D.Float ptSrc = new PointFloat(); final float[] ptDst = new float[2]; while (--numPts >= 0) { ptSrc.x = srcPts[srcOff++] - 0.5f; // See the comment in transform(Point2D...) ptSrc.y = srcPts[srcOff++] - 0.5f; // for an explanation about the 0.5 shift. final Point2D result = warp.mapDestPoint(ptSrc); dstPts[dstOff++] = (float) (result.getX() + 0.5); dstPts[dstOff++] = (float) (result.getY() + 0.5); dstOff += postIncrement; } } /** * Transforms source coordinates (usually pixel indices) into destination coordinates * (usually "real world" coordinates). */ public void transform(final double[] srcPts, int srcOff, final double[] dstPts, int dstOff, int numPts) { final int postIncrement; if (srcPts == dstPts && srcOff < dstOff) { srcOff += (numPts-1)*2; dstOff += (numPts-1)*2; postIncrement = -4; } else { postIncrement = 0; } final Point2D.Double ptSrc = new PointDouble(); final float[] ptDst = new float[2]; while (--numPts >= 0) { ptSrc.x = srcPts[srcOff++] - 0.5; // See the comment in transform(Point2D...) ptSrc.y = srcPts[srcOff++] - 0.5; // for an explanation about the 0.5 shift. final Point2D result = warp.mapDestPoint(ptSrc); dstPts[dstOff++] = result.getX() + 0.5; dstPts[dstOff++] = result.getY() + 0.5; dstOff += postIncrement; } } /** * Returns the inverse transform. * * @throws NoninvertibleTransformException if no inverse warp were specified at construction time. */ @Override public MathTransform2D inverse() throws NoninvertibleTransformException { if (inverse != null) { return inverse; } else { return (MathTransform2D) super.inverse(); } } /** * Returns a hash value for this transform. */ @Override public int hashCode() { return (int)serialVersionUID ^ super.hashCode() ^ warp.hashCode(); } /** * Compares this transform with the specified object for equality. */ @Override public boolean equals(final Object object) { if (super.equals(object)) { final WarpTransform2D that = (WarpTransform2D) object; return Utilities.equals(this.warp, that.warp); } return false; } /** * A {@code Point2D.Float} that returns itself when {@link #clone} is invoked. * This trick is used for avoiding the creation of thousands of temporary objects * when transforming an array of points using {@link Warp#mapDestPoint}. */ private static final class PointFloat extends Point2D.Float { @Override public PointFloat clone() { return this; } } /** * A {@code Point2D.Double} that returns itself when {@link #clone} is invoked. * This trick is used for avoiding the creation of thousands of temporary objects * when transforming an array of points using {@link Warp#mapDestPoint}. */ @SuppressWarnings("serial") private static final class PointDouble extends Point2D.Double { public PointDouble() { super(); } public PointDouble(double x, double y) { super(x,y); } @Override public PointDouble clone() { return this; } } /** * The provider for the {@link WarpTransform2D}. This provider constructs a JAI * {@linkplain WarpPolynomial image warp} from a set of polynomial coefficients, * and wrap it in a {@link WarpTransform2D} object. * * @version $Id$ * @author Martin Desruisseaux */ public static class Provider extends MathTransformProvider { /** Serial number for interoperability with different versions. */ private static final long serialVersionUID = -7949539694656719923L; /** Descriptor for the "{@link WarpPolynomial#getDegree degree}" parameter value. */ public static final ParameterDescriptor<Integer> DEGREE = DefaultParameterDescriptor.create( "degree", 2, 1, MAX_DEGREE); /** Descriptor for the "{@link WarpPolynomial#getXCoeffs xCoeffs}" parameter value. */ public static final ParameterDescriptor X_COEFFS = new DefaultParameterDescriptor( "xCoeffs", float[].class, null, null); /** Descriptor for the "{@link WarpPolynomial#getYCoeffs yCoeffs}" parameter value. */ public static final ParameterDescriptor Y_COEFFS = new DefaultParameterDescriptor( "yCoeffs", float[].class, null, null); /** Descriptor for the "{@link WarpPolynomial#getPreScaleX preScaleX}" parameter value. */ public static final ParameterDescriptor PRE_SCALE_X; /** Descriptor for the "{@link WarpPolynomial#getPreScaleY preScaleY}" parameter value. */ public static final ParameterDescriptor PRE_SCALE_Y; /** Descriptor for the "{@link WarpPolynomial#getPostScaleX postScaleX}" parameter value. */ public static final ParameterDescriptor POST_SCALE_X; /** Descriptor for the "{@link WarpPolynomial#getPostScaleY postScaleY}" parameter value. */ public static final ParameterDescriptor<Float> POST_SCALE_Y; static { final Float ONE = 1f; PRE_SCALE_X = DefaultParameterDescriptor.create( "preScaleX",null, Float.class, ONE, false); PRE_SCALE_Y = DefaultParameterDescriptor.create( "preScaleY", null, Float.class, ONE, false); POST_SCALE_X = DefaultParameterDescriptor.create("postScaleX", null, Float.class, ONE, false); POST_SCALE_Y = DefaultParameterDescriptor.create("postScaleY", null, Float.class, ONE, false); } /** * The parameters group. */ static final ParameterDescriptorGroup PARAMETERS = createDescriptorGroup(new NamedIdentifier[] { new NamedIdentifier(Citations.GEOTOOLS, "WarpPolynomial") }, new ParameterDescriptor[] { DEGREE, X_COEFFS, Y_COEFFS, PRE_SCALE_X, PRE_SCALE_Y, POST_SCALE_X, POST_SCALE_Y }); /** * Create a provider for warp transforms. */ public Provider() { super(2, 2, PARAMETERS); } /** * Returns the operation type. */ @Override public Class<Transformation> getOperationType() { return Transformation.class; } /** * Creates a warp transform from the specified group of parameter values. * * @param values The group of parameter values. * @return The created math transform. * @throws ParameterNotFoundException if a required parameter was not found. */ protected MathTransform createMathTransform(final ParameterValueGroup values) throws ParameterNotFoundException { final int degree = intValue(DEGREE, values); final float[] xCoeffs = (float[]) value(X_COEFFS, values); final float[] yCoeffs = (float[]) value(Y_COEFFS, values); final float preScaleX = scale( PRE_SCALE_X, values); final float preScaleY = scale( PRE_SCALE_Y, values); final float postScaleX = scale(POST_SCALE_X, values); final float postScaleY = scale(POST_SCALE_Y, values); final Warp warp; switch (degree) { case 1: warp = new WarpAffine (xCoeffs, yCoeffs, preScaleX, preScaleY, postScaleX, postScaleY); break; case 2: warp = new WarpQuadratic (xCoeffs, yCoeffs, preScaleX, preScaleY, postScaleX, postScaleY); break; case 3: warp = new WarpCubic (xCoeffs, yCoeffs, preScaleX, preScaleY, postScaleX, postScaleY); break; default: warp = new WarpGeneralPolynomial(xCoeffs, yCoeffs, preScaleX, preScaleY, postScaleX, postScaleY); break; } return new WarpTransform2D(warp, (Warp)null); } /** * Returns the parameter value for the specified operation parameter. * * @param param The parameter to look for. * @param group The parameter value group to search into. * @return The requested parameter value, or {@code 1} if none. */ private static float scale(final ParameterDescriptor param, final ParameterValueGroup group) throws ParameterNotFoundException { final Object value = value(param, group); return (value!=null) ? ((Number) value).floatValue() : 1; } } }