| /* | |
| * Copyright (c) 2005, 2014, Oracle and/or its affiliates. All rights reserved. | |
| * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. | |
| * | |
| * This code is free software; you can redistribute it and/or modify it | |
| * under the terms of the GNU General Public License version 2 only, as | |
| * published by the Free Software Foundation. | |
| * | |
| * This code 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 General Public License | |
| * version 2 for more details (a copy is included in the LICENSE file that | |
| * accompanied this code). | |
| * | |
| * You should have received a copy of the GNU General Public License version | |
| * 2 along with this work; if not, write to the Free Software Foundation, | |
| * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. | |
| * | |
| * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA | |
| * or visit www.oracle.com if you need additional information or have any | |
| * questions. | |
| */ | |
| /** | |
| * @test | |
| * @bug 4987374 8062163 | |
| * @summary Unit test for inversion methods: | |
| * | |
| * AffineTransform.createInverse(); | |
| * AffineTransform.invert(); | |
| * | |
| * @author flar | |
| * @run main TestInvertMethods | |
| */ | |
| import java.awt.geom.AffineTransform; | |
| import java.awt.geom.NoninvertibleTransformException; | |
| /* | |
| * Instances of the inner class Tester are "nodes" which take an input | |
| * AffineTransform (AT), modify it in some way and pass the modified | |
| * AT onto another Tester node. | |
| * | |
| * There is one particular Tester node of note called theVerifier. | |
| * This is a leaf node which takes the input AT and tests the various | |
| * inversion methods on that matrix. | |
| * | |
| * Most of the other Tester nodes will perform a single affine operation | |
| * on their input, such as a rotate by various angles, or a scale by | |
| * various predefined scale values, and then pass the modified AT on | |
| * to the next node in the chain which may be a verifier or another | |
| * modifier. | |
| * | |
| * The Tester instances can also be chained together using the chain | |
| * method so that we can test not only matrices modified by some single | |
| * affine operation (scale, rotate, etc.) but also composite matrices | |
| * that represent multiple operations concatenated together. | |
| */ | |
| public class TestInvertMethods { | |
| public static boolean verbose; | |
| public static final double MAX_ULPS = 2.0; | |
| public static double MAX_TX_ULPS = MAX_ULPS; | |
| public static double maxulps = 0.0; | |
| public static double maxtxulps = 0.0; | |
| public static int numtests = 0; | |
| public static void main(String argv[]) { | |
| Tester rotate = new Tester.Rotate(); | |
| Tester scale = new Tester.Scale(); | |
| Tester shear = new Tester.Shear(); | |
| Tester translate = new Tester.Translate(); | |
| if (argv.length > 1) { | |
| // This next line verifies that chaining works correctly... | |
| scale.chain(translate.chain(new Tester.Debug())).test(false); | |
| return; | |
| } | |
| verbose = (argv.length > 0); | |
| new Tester.Identity().test(true); | |
| translate.test(true); | |
| scale.test(true); | |
| rotate.test(true); | |
| shear.test(true); | |
| scale.chain(translate).test(true); | |
| rotate.chain(translate).test(true); | |
| shear.chain(translate).test(true); | |
| translate.chain(scale).test(true); | |
| translate.chain(rotate).test(true); | |
| translate.chain(shear).test(true); | |
| translate.chain(scale.chain(rotate.chain(shear))).test(false); | |
| shear.chain(rotate.chain(scale.chain(translate))).test(false); | |
| System.out.println(numtests+" tests performed"); | |
| System.out.println("Max scale and shear difference: "+maxulps+" ulps"); | |
| System.out.println("Max translate difference: "+maxtxulps+" ulps"); | |
| } | |
| public abstract static class Tester { | |
| public static AffineTransform IdentityTx = new AffineTransform(); | |
| /* | |
| * This is the leaf node that performs inversion testing | |
| * on the incoming AffineTransform. | |
| */ | |
| public static final Tester theVerifier = new Tester() { | |
| public void test(AffineTransform at, boolean full) { | |
| numtests++; | |
| AffineTransform inv1, inv2; | |
| boolean isinvertible = | |
| (Math.abs(at.getDeterminant()) >= Double.MIN_VALUE); | |
| try { | |
| inv1 = at.createInverse(); | |
| if (!isinvertible) missingNTE("createInverse", at); | |
| } catch (NoninvertibleTransformException e) { | |
| inv1 = null; | |
| if (isinvertible) extraNTE("createInverse", at); | |
| } | |
| inv2 = new AffineTransform(at); | |
| try { | |
| inv2.invert(); | |
| if (!isinvertible) missingNTE("invert", at); | |
| } catch (NoninvertibleTransformException e) { | |
| if (isinvertible) extraNTE("invert", at); | |
| } | |
| if (verbose) System.out.println("at = "+at); | |
| if (isinvertible) { | |
| if (verbose) System.out.println(" inv1 = "+inv1); | |
| if (verbose) System.out.println(" inv2 = "+inv2); | |
| if (!inv1.equals(inv2)) { | |
| report(at, inv1, inv2, | |
| "invert methods do not agree"); | |
| } | |
| inv1.concatenate(at); | |
| inv2.concatenate(at); | |
| // "Fix" some values that don't always behave | |
| // well with all the math that we've done up | |
| // to this point. | |
| // See the note on the concatfix method below. | |
| concatfix(inv1); | |
| concatfix(inv2); | |
| if (verbose) System.out.println(" at*inv1 = "+inv1); | |
| if (verbose) System.out.println(" at*inv2 = "+inv2); | |
| if (!compare(inv1, IdentityTx)) { | |
| report(at, inv1, IdentityTx, | |
| "createInverse() check failed"); | |
| } | |
| if (!compare(inv2, IdentityTx)) { | |
| report(at, inv2, IdentityTx, | |
| "invert() check failed"); | |
| } | |
| } else { | |
| if (verbose) System.out.println(" is not invertible"); | |
| } | |
| if (verbose) System.out.println(); | |
| } | |
| void missingNTE(String methodname, AffineTransform at) { | |
| throw new RuntimeException("Noninvertible was not "+ | |
| "thrown from "+methodname+ | |
| " for: "+at); | |
| } | |
| void extraNTE(String methodname, AffineTransform at) { | |
| throw new RuntimeException("Unexpected Noninvertible "+ | |
| "thrown from "+methodname+ | |
| " for: "+at); | |
| } | |
| }; | |
| /* | |
| * The inversion math may work out fairly exactly, but when | |
| * we concatenate the inversions back with the original matrix | |
| * in an attempt to restore them to the identity matrix, | |
| * then we can end up compounding errors to a fairly high | |
| * level, particularly if the component values had mantissas | |
| * that were repeating fractions. This function therefore | |
| * "fixes" the results of concatenating the inversions back | |
| * with their original matrices to get rid of small variations | |
| * in the values that should have ended up being 0.0. | |
| */ | |
| public void concatfix(AffineTransform at) { | |
| double m00 = at.getScaleX(); | |
| double m10 = at.getShearY(); | |
| double m01 = at.getShearX(); | |
| double m11 = at.getScaleY(); | |
| double m02 = at.getTranslateX(); | |
| double m12 = at.getTranslateY(); | |
| if (Math.abs(m00-1.0) < 1E-10) m00 = 1.0; | |
| if (Math.abs(m11-1.0) < 1E-10) m11 = 1.0; | |
| if (Math.abs(m02) < 1E-10) m02 = 0.0; | |
| if (Math.abs(m12) < 1E-10) m12 = 0.0; | |
| if (Math.abs(m01) < 1E-15) m01 = 0.0; | |
| if (Math.abs(m10) < 1E-15) m10 = 0.0; | |
| at.setTransform(m00, m10, | |
| m01, m11, | |
| m02, m12); | |
| } | |
| public void test(boolean full) { | |
| test(IdentityTx, full); | |
| } | |
| public void test(AffineTransform init, boolean full) { | |
| test(init, theVerifier, full); | |
| } | |
| public void test(AffineTransform init, Tester next, boolean full) { | |
| next.test(init, full); | |
| } | |
| public Tester chain(Tester next) { | |
| return new Chain(this, next); | |
| } | |
| /* | |
| * Utility node used to chain together two other nodes for | |
| * implementing the "chain" method. | |
| */ | |
| public static class Chain extends Tester { | |
| Tester prev; | |
| Tester next; | |
| public Chain(Tester prev, Tester next) { | |
| this.prev = prev; | |
| this.next = next; | |
| } | |
| public void test(AffineTransform init, boolean full) { | |
| prev.test(init, next, full); | |
| } | |
| public Tester chain(Tester next) { | |
| this.next = this.next.chain(next); | |
| return this; | |
| } | |
| } | |
| /* | |
| * Utility node for testing. | |
| */ | |
| public static class Fail extends Tester { | |
| public void test(AffineTransform init, Tester next, boolean full) { | |
| throw new RuntimeException("Debug: Forcing failure"); | |
| } | |
| } | |
| /* | |
| * Utility node for testing that chaining works. | |
| */ | |
| public static class Debug extends Tester { | |
| public void test(AffineTransform init, Tester next, boolean full) { | |
| new Throwable().printStackTrace(); | |
| next.test(init, full); | |
| } | |
| } | |
| /* | |
| * NOP node. | |
| */ | |
| public static class Identity extends Tester { | |
| public void test(AffineTransform init, Tester next, boolean full) { | |
| if (verbose) System.out.println("*Identity = "+init); | |
| next.test(init, full); | |
| } | |
| } | |
| /* | |
| * Affine rotation node. | |
| */ | |
| public static class Rotate extends Tester { | |
| public void test(AffineTransform init, Tester next, boolean full) { | |
| int inc = full ? 10 : 45; | |
| for (int i = -720; i <= 720; i += inc) { | |
| AffineTransform at2 = new AffineTransform(init); | |
| at2.rotate(i / 180.0 * Math.PI); | |
| if (verbose) System.out.println("*Rotate("+i+") = "+at2); | |
| next.test(at2, full); | |
| } | |
| } | |
| } | |
| public static final double SMALL_VALUE = .0001; | |
| public static final double LARGE_VALUE = 10000; | |
| /* | |
| * Affine scale node. | |
| */ | |
| public static class Scale extends Tester { | |
| public double fullvals[] = { | |
| // Noninvertibles | |
| 0.0, 0.0, | |
| 0.0, 1.0, | |
| 1.0, 0.0, | |
| // Invertibles | |
| SMALL_VALUE, SMALL_VALUE, | |
| SMALL_VALUE, 1.0, | |
| 1.0, SMALL_VALUE, | |
| SMALL_VALUE, LARGE_VALUE, | |
| LARGE_VALUE, SMALL_VALUE, | |
| LARGE_VALUE, LARGE_VALUE, | |
| LARGE_VALUE, 1.0, | |
| 1.0, LARGE_VALUE, | |
| 0.5, 0.5, | |
| 1.0, 1.0, | |
| 2.0, 2.0, | |
| Math.PI, Math.E, | |
| }; | |
| public double abbrevvals[] = { | |
| 0.0, 0.0, | |
| 1.0, 1.0, | |
| 2.0, 3.0, | |
| }; | |
| public void test(AffineTransform init, Tester next, boolean full) { | |
| double scales[] = (full ? fullvals : abbrevvals); | |
| for (int i = 0; i < scales.length; i += 2) { | |
| AffineTransform at2 = new AffineTransform(init); | |
| at2.scale(scales[i], scales[i+1]); | |
| if (verbose) System.out.println("*Scale("+scales[i]+", "+ | |
| scales[i+1]+") = "+at2); | |
| next.test(at2, full); | |
| } | |
| } | |
| } | |
| /* | |
| * Affine shear node. | |
| */ | |
| public static class Shear extends Tester { | |
| public double fullvals[] = { | |
| 0.0, 0.0, | |
| 0.0, 1.0, | |
| 1.0, 0.0, | |
| // Noninvertible | |
| 1.0, 1.0, | |
| SMALL_VALUE, SMALL_VALUE, | |
| SMALL_VALUE, LARGE_VALUE, | |
| LARGE_VALUE, SMALL_VALUE, | |
| LARGE_VALUE, LARGE_VALUE, | |
| Math.PI, Math.E, | |
| }; | |
| public double abbrevvals[] = { | |
| 0.0, 0.0, | |
| 0.0, 1.0, | |
| 1.0, 0.0, | |
| // Noninvertible | |
| 1.0, 1.0, | |
| }; | |
| public void test(AffineTransform init, Tester next, boolean full) { | |
| double shears[] = (full ? fullvals : abbrevvals); | |
| for (int i = 0; i < shears.length; i += 2) { | |
| AffineTransform at2 = new AffineTransform(init); | |
| at2.shear(shears[i], shears[i+1]); | |
| if (verbose) System.out.println("*Shear("+shears[i]+", "+ | |
| shears[i+1]+") = "+at2); | |
| next.test(at2, full); | |
| } | |
| } | |
| } | |
| /* | |
| * Affine translate node. | |
| */ | |
| public static class Translate extends Tester { | |
| public double fullvals[] = { | |
| 0.0, 0.0, | |
| 0.0, 1.0, | |
| 1.0, 0.0, | |
| SMALL_VALUE, SMALL_VALUE, | |
| SMALL_VALUE, LARGE_VALUE, | |
| LARGE_VALUE, SMALL_VALUE, | |
| LARGE_VALUE, LARGE_VALUE, | |
| Math.PI, Math.E, | |
| }; | |
| public double abbrevvals[] = { | |
| 0.0, 0.0, | |
| 0.0, 1.0, | |
| 1.0, 0.0, | |
| Math.PI, Math.E, | |
| }; | |
| public void test(AffineTransform init, Tester next, boolean full) { | |
| double translates[] = (full ? fullvals : abbrevvals); | |
| for (int i = 0; i < translates.length; i += 2) { | |
| AffineTransform at2 = new AffineTransform(init); | |
| at2.translate(translates[i], translates[i+1]); | |
| if (verbose) System.out.println("*Translate("+ | |
| translates[i]+", "+ | |
| translates[i+1]+") = "+at2); | |
| next.test(at2, full); | |
| } | |
| } | |
| } | |
| } | |
| public static void report(AffineTransform orig, | |
| AffineTransform at1, AffineTransform at2, | |
| String message) | |
| { | |
| System.out.println(orig+", type = "+orig.getType()); | |
| System.out.println(at1+", type = "+at1.getType()); | |
| System.out.println(at2+", type = "+at2.getType()); | |
| System.out.println("ScaleX values differ by "+ | |
| ulps(at1.getScaleX(), | |
| at2.getScaleX())+" ulps"); | |
| System.out.println("ScaleY values differ by "+ | |
| ulps(at1.getScaleY(), | |
| at2.getScaleY())+" ulps"); | |
| System.out.println("ShearX values differ by "+ | |
| ulps(at1.getShearX(), | |
| at2.getShearX())+" ulps"); | |
| System.out.println("ShearY values differ by "+ | |
| ulps(at1.getShearY(), | |
| at2.getShearY())+" ulps"); | |
| System.out.println("TranslateX values differ by "+ | |
| ulps(at1.getTranslateX(), | |
| at2.getTranslateX())+" ulps"); | |
| System.out.println("TranslateY values differ by "+ | |
| ulps(at1.getTranslateY(), | |
| at2.getTranslateY())+" ulps"); | |
| throw new RuntimeException(message); | |
| } | |
| public static boolean compare(AffineTransform at1, AffineTransform at2) { | |
| maxulps = Math.max(maxulps, ulps(at1.getScaleX(), at2.getScaleX())); | |
| maxulps = Math.max(maxulps, ulps(at1.getScaleY(), at2.getScaleY())); | |
| maxulps = Math.max(maxulps, ulps(at1.getShearX(), at2.getShearX())); | |
| maxulps = Math.max(maxulps, ulps(at1.getShearY(), at2.getShearY())); | |
| maxtxulps = Math.max(maxtxulps, | |
| ulps(at1.getTranslateX(), at2.getTranslateX())); | |
| maxtxulps = Math.max(maxtxulps, | |
| ulps(at1.getTranslateY(), at2.getTranslateY())); | |
| return (getModifiedType(at1) == getModifiedType(at2) && | |
| (compare(at1.getScaleX(), at2.getScaleX(), MAX_ULPS)) && | |
| (compare(at1.getScaleY(), at2.getScaleY(), MAX_ULPS)) && | |
| (compare(at1.getShearX(), at2.getShearX(), MAX_ULPS)) && | |
| (compare(at1.getShearY(), at2.getShearY(), MAX_ULPS)) && | |
| (compare(at1.getTranslateX(), | |
| at2.getTranslateX(), MAX_TX_ULPS)) && | |
| (compare(at1.getTranslateY(), | |
| at2.getTranslateY(), MAX_TX_ULPS))); | |
| } | |
| public static final int ANY_SCALE_MASK = | |
| (AffineTransform.TYPE_UNIFORM_SCALE | | |
| AffineTransform.TYPE_GENERAL_SCALE); | |
| public static int getModifiedType(AffineTransform at) { | |
| int type = at.getType(); | |
| // Some of the vector methods can introduce a tiny uniform scale | |
| // at some angles... | |
| if ((type & ANY_SCALE_MASK) != 0) { | |
| maxulps = Math.max(maxulps, ulps(at.getDeterminant(), 1.0)); | |
| if (ulps(at.getDeterminant(), 1.0) <= MAX_ULPS) { | |
| // Really tiny - we will ignore it | |
| type &= ~ ANY_SCALE_MASK; | |
| } | |
| } | |
| return type; | |
| } | |
| public static boolean compare(double val1, double val2, double maxulps) { | |
| if (Math.abs(val1 - val2) < 1E-15) return true; | |
| return (ulps(val1, val2) <= maxulps); | |
| } | |
| public static double ulps(double val1, double val2) { | |
| double diff = Math.abs(val1 - val2); | |
| double ulpmax = Math.min(Math.ulp(val1), Math.ulp(val2)); | |
| return (diff / ulpmax); | |
| } | |
| } |