/* * 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); } }