/*
 * Copyright (c) 2012, 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.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * 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.
 */

package javafx.scene.transform;

import java.util.Arrays;
import java.util.Collection;
import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.event.EventHandler;
import javafx.geometry.BoundingBox;
import javafx.geometry.Bounds;
import javafx.geometry.Point2D;
import javafx.geometry.Point3D;
import com.sun.javafx.scene.transform.TransformUtils;
import com.sun.javafx.test.TransformHelper;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameters;

import static org.junit.Assert.*;

@RunWith(Parameterized.class)
public class TransformOperationsTest {
    private static final Affine affine_identity = new Affine();
    private static final Affine affine_translate_only = new Affine(0, 0, 2,
                                                                   0, 0, 3);
    private static final Affine affine_translate = new Affine(1, 0, 2,
                                                              0, 1, 3);
    private static final Affine affine_scale = new Affine(4, 0, 0,
                                                          0, 5, 0);
    private static final Affine affine_sc_tr = new Affine(6, 0, 8,
                                                          0, 7, 9);
    private static final Affine affine_shear = new Affine( 0, 10, 0,
                                                          11,  0, 0);
    private static final Affine affine_sh_tr = new Affine( 0, 12, 14,
                                                          13,  0, 15);
    private static final Affine affine_sh_sc_simple = new Affine( 1, 18, 0,
                                                                 19,  1, 0);
    private static final Affine affine_sh_sc = new Affine(16, 18, 0,
                                                          19, 17, 0);
    private static final Affine affine_sh_sc_tr = new Affine(20, 21, 22,
                                                             23, 24, 25);
    private static final Affine affine_3d_tr = new Affine(1, 0, 0, 0,
                                                    0, 1, 0, 0,
                                                    0, 0, 1, 30);
    private static final Affine affine_3d_sc = new Affine(1, 0, 0, 0,
                                                    0, 1, 0, 0,
                                                    0, 0, 3, 0);
    private static final Affine affine_3d_sc_tr = new Affine(1, 0, 0, 0,
                                                       0, 1, 0, 0,
                                                       0, 0, 3, 30);
    private static final Affine affine_3d_sc2_tr3 = new Affine(1, 0, 0, 0,
                                                         0, 3, 0, 0,
                                                         0, 0, 1, 30);
    private static final Affine affine_3d_sc3_tr2 = new Affine(1, 0, 0, 25,
                                                         0, 1, 0, 0,
                                                         0, 0, 3, 0);
    private static final Affine affine_3d_withShear = new Affine(1, 5, 0, 0,
                                                           0, 1, 0, 0,
                                                           0, 0, 3, 30);
    private static final Affine affine_3d_only3d = new Affine( 1,  0, 20, 0,
                                                         0,  1, 30, 0,
                                                        11, 12, 13, 0);
    private static final Affine affine_3d_translate_only = new Affine(0, 0, 0, 10,
                                                                       0, 0, 0, 20,
                                                                       0, 0, 0, 30);
    private static final Affine affine_3d_complex = new Affine( 7,  3,  4,  5,
                                                          6,  7,  5,  9,
                                                         10, 11, 12, 13);
    private static final Affine affine_3d_complex_noninvertible =
                                                     new Affine( 2,  3,  4,  5,
                                                                 6,  7,  8,  9,
                                                                10, 11, 12, 13);
    private static final Affine affine_empty = new Affine(0, 0, 0, 0,
                                                          0, 0, 0, 0,
                                                          0, 0, 0, 0);
    private static final Affine affine_emptyZ = new Affine(1, 0, 0, 0,
                                                           0, 1, 0, 0,
                                                           0, 0, 0, 0);
    private static final Affine affine_emptyXY = new Affine(0, 0, 0, 0,
                                                            0, 0, 0, 0,
                                                            0, 0, 1, 0);
    private static final Affine affine_nonInv_translate_x = new Affine(0, 0, 2,
                                                                       0, 0, 0);
    private static final Affine affine_nonInv_translate_y = new Affine(0, 0, 0,
                                                                       0, 0, 4);
    private static final Affine affine_nonInv_translate_z = new Affine(0, 0, 0, 0,
                                                                       0, 0, 0, 0,
                                                                       0, 0, 0, 4);
    private static final Affine affine_nonInv_scale_x = new Affine(2, 0, 0,
                                                          0, 0, 0);
    private static final Affine affine_nonInv_scale_y = new Affine(0, 0, 0,
                                                          0, 2, 0);
    private static final Affine affine_nonInv_scale_xy = new Affine(2, 0, 0, 0,
                                                                    0, 2, 0, 0,
                                                                    0, 0, 0, 0);
    private static final Affine affine_nonInv_scale_z = new Affine(0, 0, 0, 0,
                                                                    0, 0, 0, 0,
                                                                    0, 0, 4, 0);
    private static final Affine affine_nonInv_shear_x = new Affine(0, 3, 0,
                                                          0, 0, 0);
    private static final Affine affine_nonInv_shear_y = new Affine(0, 0, 0,
                                                          3, 0, 0);
    private static final Affine affine_nonInv_sh_tr_x = new Affine(0, 3, 4,
                                                          0, 0, 0);
    private static final Affine affine_nonInv_sh_tr_y = new Affine(0, 0, 0,
                                                          3, 0, 4);
    private static final Affine affine_nonInv_sh_sc_tr = new Affine(0, 0, 0,
                                                             2, 3, 4);
    private static final Affine affine_nonInv_sh_sc = new Affine(0, 0, 0,
                                                          2, 3, 0);
    private static final Affine affine_nonInv_sh_tr = new Affine(0, 0, 0,
                                                          2, 0, 5);
    private static final Affine affine_nonInv_sc_tr = new Affine(0, 0, 0,
                                                          0, 6, 5);
    private static final Affine affine_nonInv_sc_tr_x = new Affine(2, 0, 4,
                                                          0, 0, 0);
    private static final Affine affine_nonInv_sc_tr_y = new Affine(0, 0, 0,
                                                          0, 2, 7);
    private static final Translate translate2d = new Translate(120, 225);
    private static final Translate translate3d = new Translate(120, 225, 346);
    private static final Translate translate3d_only = new Translate(0, 0, 346);
    private static final Translate noTranslate = new Translate(0, 0);
    private static final Scale scale2d = new Scale(0.5, 2.5, 35, 46);
    private static final Scale scale2d_x = new Scale(1.0, 2.5, 35, 46);
    private static final Scale scale2d_y = new Scale(0.5, 1.0, 35, 46);
    private static final Scale scale3d = new Scale(0.5, 2.5, 3.6, 35, 46, 55);
    private static final Scale scale3dOnly = new Scale(1.0, 1.0, 3.6);
    private static final Scale scale2dNoPivot = new Scale(0.5, 2.5);
    private static final Scale scale2dUslessPivots = new Scale(0.5, 1.0, 0.0, 45);
    private static final Scale scale2dPivot3d = new Scale(0.5, 2.5, 1.0, 35, 46, 52);
    private static final Scale scale3dNoPivot = new Scale(0.5, 2.5, 3.6);
    private static final Scale noScale = new Scale(1, 1);
    private static final Scale nonInvertible3dScale = new Scale(0.0, 2.5, 3.6, 35, 46, 55);
    private static final Scale nonInvertible2dScale = new Scale(1.3, 0.0, 35, 45);
    private static final Shear shear = new Shear(3.2, 4.3, 75, 84);
    private static final Shear shearX = new Shear(3.2, 0, 75, 84);
    private static final Shear shearY = new Shear(0, 4.3, 75, 84);
    private static final Shear shearNoPivot = new Shear(3.5, 4.3);
    private static final Shear noShear = new Shear(0, 0, 75, 84);
    private static final Rotate simpleRotate3d = new Rotate(97.5, Rotate.Y_AXIS);
    private static final Rotate rotate2d = new Rotate(97.5, 123, 456);
    private static final Rotate rotate3d = new Rotate(97.5, 33, 44, 55, new Point3D(66, 77, 88));
    private static final Rotate rotate3d2d = new Rotate(97.5, 33, 44, 55, new Point3D(0, 0, 10));
    private static final Rotate rotateZeroAxis = new Rotate(97.5, 33, 44, 55, new Point3D(0, 0, 0));
    private static final Rotate rotate3dUpsideDown2d = new Rotate(97.5, 33, 44, 55, new Point3D(0, 0, -10));
    private static final Rotate rotate2dNoPivot = new Rotate(97.5);
    private static final Rotate rotate3dNoPivot = new Rotate(97.5, new Point3D(66, 77, 88));
    private static final Rotate rotate2dPivot3d = new Rotate(97.5, 125, 126, 127, Rotate.Z_AXIS);
    private static final Rotate noRotate = new Rotate(0, Rotate.Y_AXIS);
    private static final Transform immutable_identity =
            TransformHelper.immutableTransform(1, 0, 0, 0, 1, 0);
    private static final Transform immutable_translate_only =
            TransformHelper.immutableTransform(0, 0, 2, 0, 0, 3);
    private static final Transform immutable_translate =
            TransformHelper.immutableTransform(1, 0, 2, 0, 1, 3);
    private static final Transform immutable_scale =
            TransformHelper.immutableTransform(4, 0, 0, 0, 5, 0);
    private static final Transform immutable_sc_tr =
            TransformHelper.immutableTransform(6, 0, 8, 0, 7, 9);
    private static final Transform immutable_shear =
            TransformHelper.immutableTransform( 0, 10, 0, 11,  0, 0);
    private static final Transform immutable_sh_tr =
            TransformHelper.immutableTransform( 0, 12, 14, 13,  0, 15);
    private static final Transform immutable_sh_sc_simple =
            TransformHelper.immutableTransform( 1, 18, 0, 19,  1, 0);
    private static final Transform immutable_sh_sc =
            TransformHelper.immutableTransform(16, 18, 0, 19, 17, 0);
    private static final Transform immutable_sh_sc_tr =
            TransformHelper.immutableTransform(20, 21, 22, 23, 24, 25);
    private static final Transform immutable_3d_tr =
            TransformUtils.immutableTransform(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 30);
    private static final Transform immutable_3d_sc =
            TransformUtils.immutableTransform(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 3, 0);
    private static final Transform immutable_3d_sc_tr =
            TransformUtils.immutableTransform(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 3, 30);
    private static final Transform immutable_3d_sc2_tr3 =
            TransformUtils.immutableTransform(1, 0, 0, 0, 0, 3, 0, 0, 0, 0, 1, 30);
    private static final Transform immutable_3d_sc3_tr2 =
            TransformUtils.immutableTransform(1, 0, 0, 25, 0, 1, 0, 0, 0, 0, 3, 0);
    private static final Transform immutable_3d_withShear =
            TransformUtils.immutableTransform(1, 5, 0, 0, 0, 1, 0, 0, 0, 0, 3, 30);
    private static final Transform immutable_3d_only3d =
            TransformUtils.immutableTransform(1, 0, 20, 0, 0, 1, 30, 0, 11, 12, 13, 0);
    private static final Transform immutable_3d_translate_only =
            TransformUtils.immutableTransform(0, 0, 0, 10, 0, 0, 0, 20, 0, 0, 0, 30);
    private static final Transform immutable_3d_complex =
            TransformUtils.immutableTransform(7, 3, 4, 5, 5, 7, 8, 9, 10, 11, 12, 13);
    private static final Transform immutable_3d_complex_noninvertible =
            TransformUtils.immutableTransform(2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13);
    private static final Transform immutable_empty =
            TransformUtils.immutableTransform(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0);
    private static final Transform immutable_emptyZ =
            TransformUtils.immutableTransform(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0);
    private static final Transform immutable_emptyXY =
            TransformUtils.immutableTransform(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0);
    private static final Transform immutable_nonInv_translate_x =
            TransformHelper.immutableTransform(0, 0, 2, 0, 0, 0);
    private static final Transform immutable_nonInv_translate_y =
            TransformHelper.immutableTransform(0, 0, 0, 0, 0, 4);
    private static final Transform immutable_nonInv_translate_z =
            TransformUtils.immutableTransform(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4);
    private static final Transform immutable_nonInv_scale_x =
            TransformHelper.immutableTransform(2, 0, 0, 0, 0, 0);
    private static final Transform immutable_nonInv_scale_y =
            TransformHelper.immutableTransform(0, 0, 0, 0, 2, 0);
    private static final Transform immutable_nonInv_scale_xy =
            TransformUtils.immutableTransform(2, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0);
    private static final Transform immutable_nonInv_scale_z =
            TransformUtils.immutableTransform(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0);
    private static final Transform immutable_nonInv_shear_x =
            TransformHelper.immutableTransform(0, 3, 0, 0, 0, 0);
    private static final Transform immutable_nonInv_shear_y =
            TransformHelper.immutableTransform(0, 0, 0, 3, 0, 0);
    private static final Transform immutable_nonInv_sh_tr_x =
            TransformHelper.immutableTransform(0, 3, 4, 0, 0, 0);
    private static final Transform immutable_nonInv_sh_tr_y =
            TransformHelper.immutableTransform(0, 0, 0, 3, 0, 4);
    private static final Transform immutable_nonInv_sh_sc_tr =
            TransformHelper.immutableTransform(0, 0, 0, 2, 3, 4);
    private static final Transform immutable_nonInv_sh_sc =
            TransformHelper.immutableTransform(0, 0, 0, 2, 3, 0);
    private static final Transform immutable_nonInv_sh_tr =
            TransformHelper.immutableTransform(0, 0, 0, 2, 0, 5);
    private static final Transform immutable_nonInv_sc_tr =
            TransformHelper.immutableTransform(0, 0, 0, 0, 6, 5);
    private static final Transform immutable_nonInv_sc_tr_x =
            TransformHelper.immutableTransform(2, 0, 4, 0, 0, 0);
    private static final Transform immutable_nonInv_sc_tr_y =
            TransformHelper.immutableTransform(0, 0, 0, 0, 2, 7);
    private static final Transform raw_arbitrary_nonInvertible =
            TransformHelper.rawTransform( 5,  6,  7,  8,
                                         10, 11, 12, 13,
                                         15, 16, 17, 18);
    private static final Transform raw_arbitrary =
            TransformHelper.rawTransform( 5,  6, 13,  8,
                                         10,  4, 12, 13,
                                         15, 16, 26, 18);
    private static final Transform raw_empty =
            TransformHelper.rawTransform(0, 0, 0, 0,
                                         0, 0, 0, 0,
                                         0, 0, 0, 0);
    private static final Transform raw_emptyZ =
            TransformHelper.rawTransform(1, 0, 0, 0,
                                         0, 1, 0, 0,
                                         0, 0, 0, 0);
    private static final Transform raw_emptyXY =
            TransformHelper.rawTransform(0, 0, 0, 0,
                                         0, 0, 0, 0,
                                         0, 0, 1, 0);

    private boolean listenerCalled;
    private int eventCounter;

    //BEWARE: used also in AffineOperationsTest
    @Parameters
    public static Collection getParams() {
        return Arrays.asList(new Object[][] {
            { affine_identity, true, Affine.class },            //  0
            { affine_translate, true, Affine.class },           //  1
            { affine_translate_only, true, Affine.class },      //  2
            { affine_scale, true, Affine.class },               //  3
            { affine_sc_tr, true, Affine.class },               //  4
            { affine_shear, true, Affine.class },               //  5
            { affine_sh_tr, true, Affine.class },               //  6
            { affine_sh_sc_simple, true, Affine.class },        //  7
            { affine_sh_sc, true, Affine.class },               //  8
            { affine_sh_sc_tr, true, Affine.class },            //  9
            { affine_3d_tr, false, Affine.class },              // 10
            { affine_3d_sc, false, Affine.class },              // 11
            { affine_3d_sc_tr, false, Affine.class },           // 12
            { affine_3d_sc2_tr3, false, Affine.class },         // 13
            { affine_3d_sc3_tr2, false, Affine.class },         // 14
            { affine_3d_withShear, false, Affine.class },       // 15
            { affine_3d_only3d, false, Affine.class },          // 16
            { affine_3d_translate_only, false, null },          // 17
            { affine_3d_complex, false, Affine.class },         // 18
            { affine_3d_complex_noninvertible, false, null },   // 19
            { affine_empty, false, null },                      // 20
            { affine_emptyZ, false, null },                     // 21
            { affine_emptyXY, true, null },                     // 22
            { affine_nonInv_translate_x, true, null },          // 23
            { affine_nonInv_translate_y, true, null },          // 24
            { affine_nonInv_translate_z, false, null },         // 25
            { affine_nonInv_scale_x, true, null },              // 26
            { affine_nonInv_scale_y, true, null },              // 27
            { affine_nonInv_scale_xy, false, null },            // 28
            { affine_nonInv_scale_z, false, null },             // 29
            { affine_nonInv_shear_x, true, null },              // 30
            { affine_nonInv_shear_y, true, null },              // 31
            { affine_nonInv_sh_tr_x, true, null },              // 32
            { affine_nonInv_sh_tr_y, true, null },              // 33
            { affine_nonInv_sh_sc_tr, true, null },             // 34
            { affine_nonInv_sh_sc, true, null },                // 35
            { affine_nonInv_sh_tr, true, null },                // 36
            { affine_nonInv_sc_tr, true, null },                // 37
            { affine_nonInv_sc_tr_x, true, null },              // 38
            { affine_nonInv_sc_tr_y, true, null },              // 39
            { translate2d, true, Translate.class },             // 40
            { translate3d, false, Translate.class },            // 41
            { translate3d_only, false, Translate.class },       // 42
            { noTranslate, true, Translate.class },             // 43
            { scale2d, true, Scale.class },                     // 44
            { scale2d_x, true, Scale.class },                   // 45
            { scale2d_y, true, Scale.class },                   // 46
            { scale3d, false, Scale.class },                    // 47
            { scale3dOnly, false, Scale.class },                // 48
            { scale2dNoPivot, true, Scale.class },              // 49
            { scale2dUslessPivots, true, Scale.class },         // 50
            { scale3dNoPivot, false, Scale.class },             // 51
            { scale2dPivot3d, true, Scale.class },              // 52
            { noScale, true, Scale.class },                     // 53
            { nonInvertible2dScale, true, null },               // 54
            { nonInvertible3dScale, false, null },              // 55
            { shear, true, Affine.class },                      // 56
            { shearX, true, Shear.class },                      // 57
            { shearY, true, Shear.class },                      // 58
            { shearNoPivot, true, Affine.class },               // 59
            { noShear, true, Shear.class },                     // 60
            { simpleRotate3d, false, Rotate.class},             // 61
            { rotate2d, true, Rotate.class },                   // 62
            { rotate3d, false, Rotate.class },                  // 63
            { rotate3d2d, true, Rotate.class },                 // 64
            { rotate3dUpsideDown2d, true, Rotate.class },       // 65
            { rotateZeroAxis, true, Rotate.class },             // 66
            { rotate2dNoPivot, true, Rotate.class },            // 67
            { rotate3dNoPivot, false, Rotate.class },           // 68
            { rotate2dPivot3d, true, Rotate.class },            // 69
            { noRotate, true, Rotate.class },                   // 70
            { immutable_identity, true, Affine.class },         // 71
            { immutable_translate, true, Affine.class },        // 72
            { immutable_translate_only, true, Affine.class },   // 73
            { immutable_scale, true, Affine.class },            // 74
            { immutable_sc_tr, true, Affine.class },            // 75
            { immutable_shear, true, Affine.class },            // 76
            { immutable_sh_tr, true, Affine.class },            // 77
            { immutable_sh_sc_simple, true, Affine.class },     // 78
            { immutable_sh_sc, true, Affine.class },            // 79
            { immutable_sh_sc_tr, true, Affine.class },         // 80
            { immutable_3d_tr, false, Affine.class },           // 81
            { immutable_3d_sc, false, Affine.class },           // 82
            { immutable_3d_sc_tr, false, Affine.class },        // 83
            { immutable_3d_sc2_tr3, false, Affine.class },      // 84
            { immutable_3d_sc3_tr2, false, Affine.class },      // 85
            { immutable_3d_withShear, false, Affine.class },    // 86
            { immutable_3d_only3d, false, Affine.class },       // 87
            { immutable_3d_translate_only, false, null },       // 88
            { immutable_3d_complex, false, Affine.class },      // 89
            { immutable_3d_complex_noninvertible, false, null },// 90
            { immutable_empty, false, null },                   // 91
            { immutable_emptyZ, false, null },                  // 92
            { immutable_emptyXY, true, null },                  // 93
            { immutable_nonInv_translate_x, true, null },       // 94
            { immutable_nonInv_translate_y, true, null },       // 95
            { immutable_nonInv_translate_z, false, null },      // 96
            { immutable_nonInv_scale_x, true, null },           // 97
            { immutable_nonInv_scale_y, true, null },           // 98
            { immutable_nonInv_scale_xy, false, null },         // 99
            { immutable_nonInv_scale_z, false, null },          //100
            { immutable_nonInv_shear_x, true, null },           //101
            { immutable_nonInv_shear_y, true, null },           //102
            { immutable_nonInv_sh_tr_x, true, null },           //103
            { immutable_nonInv_sh_tr_y, true, null },           //104
            { immutable_nonInv_sh_sc_tr, true, null },          //105
            { immutable_nonInv_sh_sc, true, null },             //106
            { immutable_nonInv_sh_tr, true, null },             //107
            { immutable_nonInv_sc_tr, true, null },             //108
            { immutable_nonInv_sc_tr_x, true, null },           //109
            { immutable_nonInv_sc_tr_y, true, null },           //110
            { raw_arbitrary, false, Affine.class },             //111
            { raw_arbitrary_nonInvertible, false, null },       //112
            { raw_empty, false, null },                         //113
            { raw_emptyZ, false, null },                        //114
            { raw_emptyXY, true, null },                        //115
        });
    }

    private Transform t;
    private Transform it;
    private boolean is2d, isIdentity;
    private boolean isInvertible;
    private Class inverseType;

    public TransformOperationsTest(Transform t, boolean twoDee, Class inverseType) {
        this.t = t;
        this.is2d = twoDee;
        this.isIdentity =
               (t.getMxx() == 1 && t.getMxy() == 0 && t.getMxz() == 0 && t.getTx() == 0
             && t.getMyx() == 0 && t.getMyy() == 1 && t.getMyz() == 0 && t.getTy() == 0
             && t.getMzx() == 0 && t.getMzy() == 0 && t.getMzz() == 1 && t.getTz() == 0);

        this.it = null;
        this.inverseType = inverseType;
        this.isInvertible = (TransformHelper.determinant(t) != 0);
        if (isInvertible) {
            try {
                it = TransformHelper.invert(t);
            } catch (NonInvertibleTransformException e) {
                // error in test
                throw new RuntimeException("Test is wrong, it must be invertible");
            }
        } else {
            // to avoid non-null checks everywhere
            it = new Affine();
        }
    }

    @Test
    public void testClone() {
        final double mxx = t.getMxx();
        final double mxy = t.getMxy();
        final double mxz = t.getMxz();
        final double tx = t.getTx();
        final double myx = t.getMyx();
        final double myy = t.getMyy();
        final double myz = t.getMyz();
        final double ty = t.getTy();
        final double mzx = t.getMzx();
        final double mzy = t.getMzy();
        final double mzz = t.getMzz();
        final double tz = t.getTz();

        Transform clone = t.clone();

        TransformHelper.assertMatrix(clone,
                mxx, mxy, mxz, tx, myx, myy, myz, ty, mzx, mzy, mzz, tz);

        if (!TransformHelper.modify(clone, 42)) {
            // cannot modify, nothing else to test
            return;
        }

        TransformHelper.assertMatrixDiffers(clone,
                mxx, mxy, mxz, tx, myx, myy, myz, ty, mzx, mzy, mzz, tz);

        TransformHelper.assertMatrix(t,
                mxx, mxy, mxz, tx, myx, myy, myz, ty, mzx, mzy, mzz, tz);
    }

    private Class getExpectedConcatenationClass(Transform t1, Transform t2) {
        Class c1 = t1.getClass();
        Class c2 = t2.getClass();

        if (c1 == Translate.class && c2 == Translate.class) {
            return Translate.class;
        }

        if (c1 == Translate.class && c2 == Scale.class) {
            Translate t = (Translate) t1;
            Scale s = (Scale) t2;

            if ((t.getX() == 0.0 || s.getX() != 1.0) &&
                    (t.getY() == 0.0 || s.getY() != 1.0) &&
                    (t.getZ() == 0.0 || s.getZ() != 1.0)) {
                return Scale.class;
            }
        }

        if (c1 == Scale.class && c2 == Translate.class) {
            Scale s = (Scale) t1;
            Translate tr = (Translate) t2;

            if ((tr.getX() == 0.0 || (s.getX() != 1.0 && s.getX() != 0.0)) &&
                    (tr.getY() == 0.0 || (s.getY() != 1.0 && s.getY() != 0.0)) &&
                    (tr.getZ() == 0.0 || (s.getZ() != 1.0 && s.getY() != 0.0))) {
                return Scale.class;
            }
        }

        if (c1 == Scale.class && c2 == Scale.class) {
            Scale s1 = (Scale) t1;
            Scale s2 = (Scale) t2;

            if (s1.getPivotX() == s2.getPivotX() &&
                    s1.getPivotY() == s2.getPivotY() &&
                    s1.getPivotZ() == s2.getPivotZ()) {
                return Scale.class;
            }
        }

        if (c1 == Rotate.class && c2 == Rotate.class) {
            Rotate r1 = (Rotate) t1;
            Rotate r2 = (Rotate) t2;
            if (r1.getAxis().normalize().equals(r2.getAxis().normalize()) &&
                    r1.getPivotX() == r2.getPivotX() &&
                    r1.getPivotY() == r2.getPivotY() &&
                    r1.getPivotZ() == r2.getPivotZ()) {
                return Rotate.class;
            }
        }

        return Affine.class;
    }

    @Test
    public void testCreateConcatenation() {
        int counter = 0;
        for (Object o : TransformOperationsTest.getParams()) {
            Object[] arr = (Object[]) o;
            Transform other = (Transform) arr[0];

            Transform res = TransformHelper.concatenate(t, other);
            Transform conc = t.createConcatenation(other);

            TransformHelper.assertMatrix("Concatenating with #" + counter,
                    conc, res);
            assertSame("Concatenating with #" + counter,
                    getExpectedConcatenationClass(t, other),
                    conc.getClass());
            counter++;
        }
    }

    @Test(expected=NullPointerException.class)
    public void testCreateConcatenationNullTransform() {
        t.createConcatenation(null);
    }

    @Test
    public void testCreateInverse() {
        Transform res = null;
        try {
            res = t.createInverse();
        } catch(NonInvertibleTransformException e) {
            if (isInvertible) {
                e.printStackTrace();
                fail("NonInvertibleTransformException thrown for invertible transform");
            } else {
                // ok
                return;
            }
        }

        if (!isInvertible) {
            fail("Should have thrown NonInvertibleTransformException");
        }

        assertNotNull(res);
        assertSame(inverseType, res.getClass());
        TransformHelper.assertMatrix(res, it);
    }

    @Test
    public void createInverseShouldUpdateCache() {
        Transform ct = t.clone();
        Transform res = null;
        boolean canInvert = isInvertible;
        try {
            res = ct.createInverse();
        } catch(NonInvertibleTransformException e) {
            if (canInvert) {
                e.printStackTrace();
                fail("NonInvertibleTransformException thrown for invertible transform");
            } else {
                // ok
                return;
            }
        }

        if (!canInvert) {
            fail("Should have thrown NonInvertibleTransformException");
        }

        assertNotNull(res);
        assertSame(inverseType, res.getClass());
        TransformHelper.assertMatrix(res, it);

        // modify the matrix to check the cache keeps up

        TransformHelper.modify(ct, 43);
        Transform inv = null;
        try {
            inv = TransformHelper.invert(ct);
            canInvert = true;
        } catch (NonInvertibleTransformException e) {
            canInvert = false;
        }

        try {
            res = ct.createInverse();
        } catch(NonInvertibleTransformException e) {
            if (canInvert) {
                e.printStackTrace();
                fail("NonInvertibleTransformException thrown for invertible transform");
            } else {
                // ok
                return;
            }
        }

        if (!isInvertible) {
            fail("Should have thrown NonInvertibleTransformException");
        }

        assertNotNull(res);
        TransformHelper.assertMatrix(res, inv);

        // emulate garbage collection of the cache to check it's renewed
        ct.clearInverseCache();

        try {
            res = ct.createInverse();
        } catch(NonInvertibleTransformException e) {
            if (canInvert) {
                e.printStackTrace();
                fail("NonInvertibleTransformException thrown for invertible transform");
            } else {
                // ok
                return;
            }
        }

        if (!isInvertible) {
            fail("Should have thrown NonInvertibleTransformException");
        }

        assertNotNull(res);
        TransformHelper.assertMatrix(res, inv);
    }

    @Test
    public void testTransformPoint3d() {
        Point3D p = new Point3D(12, -18, 30);
        Point3D expected = new Point3D(
            t.getMxx() * 12 - t.getMxy() * 18 + t.getMxz() * 30 + t.getTx(),
            t.getMyx() * 12 - t.getMyy() * 18 + t.getMyz() * 30 + t.getTy(),
            t.getMzx() * 12 - t.getMzy() * 18 + t.getMzz() * 30 + t.getTz());


        Point3D result = t.transform(p);
        assertEquals(expected.getX(), result.getX(), 0.00001);
        assertEquals(expected.getY(), result.getY(), 0.00001);
        assertEquals(expected.getZ(), result.getZ(), 0.00001);

        result = t.transform(12, -18, 30);
        assertEquals(expected.getX(), result.getX(), 0.00001);
        assertEquals(expected.getY(), result.getY(), 0.00001);
        assertEquals(expected.getZ(), result.getZ(), 0.00001);
    }

    @Test(expected=NullPointerException.class)
    public void testTransformNullPoint3D() {
        t.transform((Point3D) null);
    }

    @Test
    public void testTransformPoint2d() {

        Point2D p = new Point2D(12, -18);
        Point2D expected = new Point2D(
            t.getMxx() * 12 - t.getMxy() * 18 + t.getTx(),
            t.getMyx() * 12 - t.getMyy() * 18 + t.getTy());

        try {
            Point2D result = t.transform(p);
            if (!is2d) {
                fail("Should have thrown ISE");
            }
            assertEquals(expected.getX(), result.getX(), 0.00001);
            assertEquals(expected.getY(), result.getY(), 0.00001);
        } catch (IllegalStateException e) {
            if (is2d) {
                fail("Wrong exception thrown");
            }
        }

        try {
            Point2D result = t.transform(12, -18);
            if (!is2d) {
                fail("Should have thrown ISE");
            }
            assertEquals(expected.getX(), result.getX(), 0.00001);
            assertEquals(expected.getY(), result.getY(), 0.00001);
        } catch (IllegalStateException e) {
            if (is2d) {
                fail("Wrong exception thrown");
            }
        }
    }

    @Test(expected=NullPointerException.class)
    public void testTransformNullPoint2D() {
        t.transform((Point2D) null);
    }

    @Test
    public void testDeltaTransformPoint3d() {
        Point3D p = new Point3D(12, -18, 30);
        Point3D expected = new Point3D(
            t.getMxx() * 12 - t.getMxy() * 18 + t.getMxz() * 30,
            t.getMyx() * 12 - t.getMyy() * 18 + t.getMyz() * 30,
            t.getMzx() * 12 - t.getMzy() * 18 + t.getMzz() * 30);


        Point3D result = t.deltaTransform(p);
        assertEquals(expected.getX(), result.getX(), 0.00001);
        assertEquals(expected.getY(), result.getY(), 0.00001);
        assertEquals(expected.getZ(), result.getZ(), 0.00001);

        result = t.deltaTransform(12, -18, 30);
        assertEquals(expected.getX(), result.getX(), 0.00001);
        assertEquals(expected.getY(), result.getY(), 0.00001);
        assertEquals(expected.getZ(), result.getZ(), 0.00001);
    }

    @Test(expected=NullPointerException.class)
    public void testDeltaTransformNullPoint3D() {
        t.deltaTransform((Point3D) null);
    }

    @Test
    public void testDeltaTransformPoint2d() {

        Point2D p = new Point2D(12, -18);
        Point2D expected = new Point2D(
            t.getMxx() * 12 - t.getMxy() * 18,
            t.getMyx() * 12 - t.getMyy() * 18);

        try {
            Point2D result = t.deltaTransform(p);
            if (!is2d) {
                fail("Should have thrown ISE");
            }
            assertEquals(expected.getX(), result.getX(), 0.00001);
            assertEquals(expected.getY(), result.getY(), 0.00001);
        } catch (IllegalStateException e) {
            if (is2d) {
                fail("Wrong exception thrown");
            }
        }

        try {
            Point2D result = t.deltaTransform(12, -18);
            if (!is2d) {
                fail("Should have thrown ISE");
            }
            assertEquals(expected.getX(), result.getX(), 0.00001);
            assertEquals(expected.getY(), result.getY(), 0.00001);
        } catch (IllegalStateException e) {
            if (is2d) {
                fail("Wrong exception thrown");
            }
        }
    }

    @Test(expected=NullPointerException.class)
    public void testDeltaTransformNullPoint2D() {
        t.deltaTransform((Point2D) null);
    }

    @Test
    public void testTransformBounds() {
        Bounds result = t.transform(new BoundingBox(10, 11, 12, 13, 14, 15));

        Point3D[] points = new Point3D[] {
                new Point3D(10, 11, 12),
                new Point3D(10, 11, 27),
                new Point3D(10, 25, 12),
                new Point3D(10, 25, 27),
                new Point3D(23, 11, 12),
                new Point3D(23, 11, 27),
                new Point3D(23, 25, 12),
                new Point3D(23, 25, 27),
        };

        Point3D expected1 = new Point3D(Double.MAX_VALUE, Double.MAX_VALUE, Double.MAX_VALUE);
        Point3D expected2 = new Point3D(-Double.MAX_VALUE, -Double.MAX_VALUE, -Double.MAX_VALUE);

        for (Point3D p : points) {
            Point3D tp = new Point3D(
                    t.getMxx() * p.getX() + t.getMxy() * p.getY() + t.getMxz() * p.getZ() + t.getTx(),
                    t.getMyx() * p.getX() + t.getMyy() * p.getY() + t.getMyz() * p.getZ() + t.getTy(),
                    t.getMzx() * p.getX() + t.getMzy() * p.getY() + t.getMzz() * p.getZ() + t.getTz());
            expected1 = new Point3D(Math.min(expected1.getX(), tp.getX()), Math.min(expected1.getY(), tp.getY()),
                    Math.min(expected1.getZ(), tp.getZ()));
            expected2 = new Point3D(Math.max(expected2.getX(), tp.getX()), Math.max(expected2.getY(), tp.getY()),
                    Math.max(expected2.getZ(), tp.getZ()));

        }

        assertEquals(expected1.getX(), result.getMinX(), 0.00001);
        assertEquals(expected1.getY(), result.getMinY(), 0.00001);
        assertEquals(expected1.getZ(), result.getMinZ(), 0.00001);
        assertEquals(expected2.getX(), result.getMaxX(), 0.00001);
        assertEquals(expected2.getY(), result.getMaxY(), 0.00001);
        assertEquals(expected2.getZ(), result.getMaxZ(), 0.00001);
    }

    @Test(expected=NullPointerException.class)
    public void testTransformNullBounds() {
        t.transform((Bounds) null);
    }

    @Test
    public void testTransform2DPoints() {
        double[] srcPts = new double[] { 0, 1, 2, 3, 4, 5, 6 };
        double[] dstPts = new double[] { 1, 2, 3, 4, 5, 6 };

        Point2D expected1 = new Point2D(
            t.getMxx() * 3 + t.getMxy() * 4 + t.getTx(),
            t.getMyx() * 3 + t.getMyy() * 4 + t.getTy());

        Point2D expected2 = new Point2D(
            t.getMxx() * 5 + t.getMxy() * 6 + t.getTx(),
            t.getMyx() * 5 + t.getMyy() * 6 + t.getTy());

        try {
            t.transform2DPoints(srcPts, 3, dstPts, 1, 2);
            if (!is2d) {
                fail("Should have thrown ISE");
            }

            assertEquals(1, dstPts[0], 0.00001);
            assertEquals(expected1.getX(), dstPts[1], 0.00001);
            assertEquals(expected1.getY(), dstPts[2], 0.00001);
            assertEquals(expected2.getX(), dstPts[3], 0.00001);
            assertEquals(expected2.getY(), dstPts[4], 0.00001);
            assertEquals(6, dstPts[5], 0.00001);
        } catch (IllegalStateException e) {
            if (is2d) {
                fail("Wrong exception thrown");
            }
        }
    }

    @Test(expected=NullPointerException.class)
    public void testTransform2DPointsBothPtsNull() {
        t.transform2DPoints(null, 2, null, 0, 0);
    }

    @Test(expected=NullPointerException.class)
    public void testTransform2DPointsSrcPtsNull() {
        t.transform2DPoints(null, 2, new double[] { 1, 2 }, 0, 0);
    }

    @Test(expected=NullPointerException.class)
    public void testTransform2DPointsDstPtsNull() {
        t.transform2DPoints(new double[] { 1, 2, 3, 4 }, 2, null, 0, 0);
    }

    @Test
    public void testTransform2DPointsWithOverlap() {
        double[] srcPts = new double[] { 0, 1, 2, 3, 4, 5, 6, 7, 8 };

        Point2D expected1 = new Point2D(
            t.getMxx() * 2 + t.getMxy() * 3 + t.getTx(),
            t.getMyx() * 2 + t.getMyy() * 3 + t.getTy());

        Point2D expected2 = new Point2D(
            t.getMxx() * 4 + t.getMxy() * 5 + t.getTx(),
            t.getMyx() * 4 + t.getMyy() * 5 + t.getTy());

        try {
            t.transform2DPoints(srcPts, 2, srcPts, 4, 2);
            if (!is2d) {
                fail("Should have thrown ISE");
            }

            assertEquals(0, srcPts[0], 0.00001);
            assertEquals(1, srcPts[1], 0.00001);
            assertEquals(2, srcPts[2], 0.00001);
            assertEquals(3, srcPts[3], 0.00001);
            assertEquals(expected1.getX(), srcPts[4], 0.00001);
            assertEquals(expected1.getY(), srcPts[5], 0.00001);
            assertEquals(expected2.getX(), srcPts[6], 0.00001);
            assertEquals(expected2.getY(), srcPts[7], 0.00001);
            assertEquals(8, srcPts[8], 0.00001);
        } catch (IllegalStateException e) {
            if (is2d) {
                fail("Wrong exception thrown");
            }
        }
    }

    @Test(expected=IndexOutOfBoundsException.class)
    public void testTransform2DPointsSrcOut() {
        double[] srcPts = new double[] { 0, 1, 2, 3, 4, 5, 6, 7 };
        double[] dstPts = new double[] { 1, 2, 3, 4, 5, 6 };

        try {
            t.transform2DPoints(srcPts, 3, dstPts, 0, 3);
        } catch (IllegalStateException e) {
            if (!is2d) {
                throw new IndexOutOfBoundsException("expected result");
            }
        }
    }

    @Test(expected=IndexOutOfBoundsException.class)
    public void testTransform2DPointsDstOut() {
        double[] srcPts = new double[] { 0, 1, 2, 3, 4, 5, 6, 7 };
        double[] dstPts = new double[] { 1 };

        try {
            t.transform2DPoints(srcPts, 1, dstPts, 0, 2);
        } catch (IllegalStateException e) {
            if (!is2d) {
                throw new IndexOutOfBoundsException("expected result");
            }
        }
    }

    @Test
    public void testTransform3DPoints() {
        double[] srcPts = new double[] { 0, 1, 2, 3, 4, 5, 6, 7, 8 };
        double[] dstPts = new double[] { 1, 2, 3, 4, 5, 6, 7, 8 };

        Point3D expected1 = new Point3D(
            t.getMxx() * 3 + t.getMxy() * 4 + t.getMxz() * 5 + t.getTx(),
            t.getMyx() * 3 + t.getMyy() * 4 + t.getMyz() * 5 + t.getTy(),
            t.getMzx() * 3 + t.getMzy() * 4 + t.getMzz() * 5 + t.getTz());

        Point3D expected2 = new Point3D(
            t.getMxx() * 6 + t.getMxy() * 7 + t.getMxz() * 8 + t.getTx(),
            t.getMyx() * 6 + t.getMyy() * 7 + t.getMyz() * 8 + t.getTy(),
            t.getMzx() * 6 + t.getMzy() * 7 + t.getMzz() * 8 + t.getTz());

        t.transform3DPoints(srcPts, 3, dstPts, 1, 2);

        assertEquals(1, dstPts[0], 0.00001);
        assertEquals(expected1.getX(), dstPts[1], 0.00001);
        assertEquals(expected1.getY(), dstPts[2], 0.00001);
        assertEquals(expected1.getZ(), dstPts[3], 0.00001);
        assertEquals(expected2.getX(), dstPts[4], 0.00001);
        assertEquals(expected2.getY(), dstPts[5], 0.00001);
        assertEquals(expected2.getZ(), dstPts[6], 0.00001);
        assertEquals(8, dstPts[7], 0.00001);
    }

    @Test(expected=NullPointerException.class)
    public void testTransform3DPointsBothPtsNull() {
        t.transform3DPoints(null, 2, null, 0, 0);
    }

    @Test(expected=NullPointerException.class)
    public void testTransform3DPointsSrcPtsNull() {
        t.transform3DPoints(null, 2, new double[] { 1, 2, 3 }, 0, 0);
    }

    @Test(expected=NullPointerException.class)
    public void testTransform3DPointsDstPtsNull() {
        t.transform3DPoints(new double[] { 1, 2, 3, 4 }, 2, null, 0, 0);
    }

    @Test
    public void testTransform3DPointsWithOverlap() {
        double[] srcPts = new double[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };

        Point3D expected1 = new Point3D(
            t.getMxx() * 2 + t.getMxy() * 3 + t.getMxz() * 4 + t.getTx(),
            t.getMyx() * 2 + t.getMyy() * 3 + t.getMyz() * 4 + t.getTy(),
            t.getMzx() * 2 + t.getMzy() * 3 + t.getMzz() * 4 + t.getTz());

        Point3D expected2 = new Point3D(
            t.getMxx() * 5 + t.getMxy() * 6 + t.getMxz() * 7 + t.getTx(),
            t.getMyx() * 5 + t.getMyy() * 6 + t.getMyz() * 7 + t.getTy(),
            t.getMzx() * 5 + t.getMzy() * 6 + t.getMzz() * 7 + t.getTz());

        t.transform3DPoints(srcPts, 2, srcPts, 3, 2);

        assertEquals(0, srcPts[0], 0.00001);
        assertEquals(1, srcPts[1], 0.00001);
        assertEquals(2, srcPts[2], 0.00001);
        assertEquals(expected1.getX(), srcPts[3], 0.00001);
        assertEquals(expected1.getY(), srcPts[4], 0.00001);
        assertEquals(expected1.getZ(), srcPts[5], 0.00001);
        assertEquals(expected2.getX(), srcPts[6], 0.00001);
        assertEquals(expected2.getY(), srcPts[7], 0.00001);
        assertEquals(expected2.getZ(), srcPts[8], 0.00001);
        assertEquals(9, srcPts[9], 0.00001);
    }

    @Test(expected=IndexOutOfBoundsException.class)
    public void testTransform3DPointsSrcOut() {
        double[] srcPts = new double[] { 0, 1, 2, 3, 4, 5, 6, 7 };
        double[] dstPts = new double[] { 1, 2, 3, 4, 5, 6 };

        t.transform3DPoints(srcPts, 6, dstPts, 0, 1);
    }

    @Test(expected=IndexOutOfBoundsException.class)
    public void testTransform3DPointsDstOut() {
        double[] srcPts = new double[] { 0, 1, 2, 3, 4, 5, 6, 7 };
        double[] dstPts = new double[] { 1 };

        t.transform3DPoints(srcPts, 1, dstPts, 0, 1);
    }

    @Test
    public void testInverseTransformPoint3d() throws Exception {
        Point3D p = new Point3D(12, -18, 30);

        Point3D expected = new Point3D(
            it.getMxx() * 12 - it.getMxy() * 18 + it.getMxz() * 30 + it.getTx(),
            it.getMyx() * 12 - it.getMyy() * 18 + it.getMyz() * 30 + it.getTy(),
            it.getMzx() * 12 - it.getMzy() * 18 + it.getMzz() * 30 + it.getTz());

        try {
            Point3D result = t.inverseTransform(p);
            if (!isInvertible) {
                fail("Should have thrown NonInvertibleTransformException");
            }
            assertEquals(expected.getX(), result.getX(), 0.00001);
            assertEquals(expected.getY(), result.getY(), 0.00001);
            assertEquals(expected.getZ(), result.getZ(), 0.00001);
        } catch (NonInvertibleTransformException e) {
            if (isInvertible) {
                fail("Wrong exception thrown");
            }
        }

        try {
            Point3D result = t.inverseTransform(12, -18, 30);
            if (!isInvertible) {
                fail("Should have thrown NonInvertibleTransformException");
            }
            assertEquals(expected.getX(), result.getX(), 0.00001);
            assertEquals(expected.getY(), result.getY(), 0.00001);
            assertEquals(expected.getZ(), result.getZ(), 0.00001);
        } catch (NonInvertibleTransformException e) {
            if (isInvertible) {
                fail("Wrong exception thrown");
            }
        }
    }

    @Test(expected=NullPointerException.class)
    public void testInverseTransformNullPoint3D()
            throws NonInvertibleTransformException {
        t.inverseTransform((Point3D) null);
    }

    @Test
    public void testInverseTransformPoint2d() throws Exception {

        Point2D p = new Point2D(12, -18);
        Point2D expected = new Point2D(
            it.getMxx() * 12 - it.getMxy() * 18 + it.getTx(),
            it.getMyx() * 12 - it.getMyy() * 18 + it.getTy());

        try {
            Point2D result = t.inverseTransform(p);
            if (!is2d) {
                fail("Should have thrown ISE");
            }
            if (!isInvertible) {
                fail("Should have thrown NonInvertibleTransformException");
            }
            assertEquals(expected.getX(), result.getX(), 0.00001);
            assertEquals(expected.getY(), result.getY(), 0.00001);
        } catch (IllegalStateException e) {
            if (is2d) {
                fail("Wrong exception thrown");
            }
        } catch (NonInvertibleTransformException e) {
            if (isInvertible) {
                fail("Wrong exception thrown");
            }
        }

        try {
            Point2D result = t.inverseTransform(12, -18);
            if (!is2d) {
                fail("Should have thrown ISE");
            }
            if (!isInvertible) {
                fail("Should have thrown NonInvertibleTransformException");
            }
            assertEquals(expected.getX(), result.getX(), 0.00001);
            assertEquals(expected.getY(), result.getY(), 0.00001);
        } catch (IllegalStateException e) {
            if (is2d) {
                fail("Wrong exception thrown");
            }
        } catch (NonInvertibleTransformException e) {
            if (isInvertible) {
                fail("Wrong exception thrown");
            }
        }
    }

    @Test(expected=NullPointerException.class)
    public void testInverseTransformNullPoint2D()
            throws NonInvertibleTransformException {
        t.inverseTransform((Point2D) null);
    }

    @Test
    public void testInverseDeltaTransformPoint3d() throws Exception {
        Point3D p = new Point3D(12, -18, 30);
        Point3D expected = new Point3D(
            it.getMxx() * 12 - it.getMxy() * 18 + it.getMxz() * 30,
            it.getMyx() * 12 - it.getMyy() * 18 + it.getMyz() * 30,
            it.getMzx() * 12 - it.getMzy() * 18 + it.getMzz() * 30);

        try {
            Point3D result = t.inverseDeltaTransform(p);
            if (!isInvertible) {
                fail("Should have thrown NonInvertibleTransformException");
            }
            assertEquals(expected.getX(), result.getX(), 0.00001);
            assertEquals(expected.getY(), result.getY(), 0.00001);
            assertEquals(expected.getZ(), result.getZ(), 0.00001);
        } catch (NonInvertibleTransformException e) {
            if (isInvertible) {
                fail("Wrong exception thrown");
            }
        }

        try {
            Point3D result = t.inverseDeltaTransform(12, -18, 30);
            if (!isInvertible) {
                fail("Should have thrown NonInvertibleTransformException");
            }
            assertEquals(expected.getX(), result.getX(), 0.00001);
            assertEquals(expected.getY(), result.getY(), 0.00001);
            assertEquals(expected.getZ(), result.getZ(), 0.00001);
        } catch (NonInvertibleTransformException e) {
            if (isInvertible) {
                fail("Wrong exception thrown");
            }
        }
    }

    @Test(expected=NullPointerException.class)
    public void testInverseDeltaTransformNullPoint3D()
            throws NonInvertibleTransformException {
        t.inverseDeltaTransform((Point3D) null);
    }

    @Test
    public void testInverseDeltaTransformPoint2d() throws Exception {

        Point2D p = new Point2D(12, -18);
        Point2D expected = new Point2D(
            it.getMxx() * 12 - it.getMxy() * 18,
            it.getMyx() * 12 - it.getMyy() * 18);

        try {
            Point2D result = t.inverseDeltaTransform(p);
            if (!is2d) {
                fail("Should have thrown ISE");
            }
            if (!isInvertible) {
                fail("Should have thrown NonInvertibleTransformException");
            }
            assertEquals(expected.getX(), result.getX(), 0.00001);
            assertEquals(expected.getY(), result.getY(), 0.00001);
        } catch (IllegalStateException e) {
            if (is2d) {
                fail("Wrong exception thrown");
            }
        } catch (NonInvertibleTransformException e) {
            if (isInvertible) {
                fail("Wrong exception thrown");
            }
        }

        try {
            Point2D result = t.inverseDeltaTransform(12, -18);
            if (!is2d) {
                fail("Should have thrown ISE");
            }
            if (!isInvertible) {
                fail("Should have thrown NonInvertibleTransformException");
            }
            assertEquals(expected.getX(), result.getX(), 0.00001);
            assertEquals(expected.getY(), result.getY(), 0.00001);
        } catch (IllegalStateException e) {
            if (is2d) {
                fail("Wrong exception thrown");
            }
        } catch (NonInvertibleTransformException e) {
            if (isInvertible) {
                fail("Wrong exception thrown");
            }
        }
    }

    @Test(expected=NullPointerException.class)
    public void testInverseDeltaTransformNullPoint2D()
            throws NonInvertibleTransformException {
        t.inverseDeltaTransform((Point2D) null);
    }

    @Test
    public void testInverseTransformBounds() throws Exception {
        Bounds result = null;
        try {
            result = t.inverseTransform(new BoundingBox(10, 11, 12, 13, 14, 15));
            if (!isInvertible) {
                fail("Should have thrown NonInvertibleTransformException");
            }

            Point3D[] points = new Point3D[] {
                    new Point3D(10, 11, 12),
                    new Point3D(10, 11, 27),
                    new Point3D(10, 25, 12),
                    new Point3D(10, 25, 27),
                    new Point3D(23, 11, 12),
                    new Point3D(23, 11, 27),
                    new Point3D(23, 25, 12),
                    new Point3D(23, 25, 27),
            };

            Point3D expected1 = new Point3D(Double.MAX_VALUE, Double.MAX_VALUE, Double.MAX_VALUE);
            Point3D expected2 = new Point3D(-Double.MAX_VALUE, -Double.MAX_VALUE, -Double.MAX_VALUE);

            for (Point3D p : points) {
                Point3D tp = new Point3D(
                        it.getMxx() * p.getX() + it.getMxy() * p.getY() + it.getMxz() * p.getZ() + it.getTx(),
                        it.getMyx() * p.getX() + it.getMyy() * p.getY() + it.getMyz() * p.getZ() + it.getTy(),
                        it.getMzx() * p.getX() + it.getMzy() * p.getY() + it.getMzz() * p.getZ() + it.getTz());
                expected1 = new Point3D(Math.min(expected1.getX(), tp.getX()), Math.min(expected1.getY(), tp.getY()),
                        Math.min(expected1.getZ(), tp.getZ()));
                expected2 = new Point3D(Math.max(expected2.getX(), tp.getX()), Math.max(expected2.getY(), tp.getY()),
                        Math.max(expected2.getZ(), tp.getZ()));

            }

            assertEquals(expected1.getX(), result.getMinX(), 0.00001);
            assertEquals(expected1.getY(), result.getMinY(), 0.00001);
            assertEquals(expected1.getZ(), result.getMinZ(), 0.00001);
            assertEquals(expected2.getX(), result.getMaxX(), 0.00001);
            assertEquals(expected2.getY(), result.getMaxY(), 0.00001);
            assertEquals(expected2.getZ(), result.getMaxZ(), 0.00001);

        } catch (NonInvertibleTransformException e) {
            if (isInvertible) {
                fail("Wrong exception thrown");
            }
            return;
        }
    }

    @Test(expected=NullPointerException.class)
    public void testInverseTransformNullBounds()
            throws NonInvertibleTransformException {
        t.inverseTransform((Bounds) null);
    }

    @Test
    public void testInverseTransform2DPoints() throws Exception {
        double[] srcPts = new double[] { 0, 1, 2, 3, 4, 5, 6 };
        double[] dstPts = new double[] { 1, 2, 3, 4, 5, 6 };

        Point2D expected1 = new Point2D(
            it.getMxx() * 3 + it.getMxy() * 4 + it.getTx(),
            it.getMyx() * 3 + it.getMyy() * 4 + it.getTy());

        Point2D expected2 = new Point2D(
            it.getMxx() * 5 + it.getMxy() * 6 + it.getTx(),
            it.getMyx() * 5 + it.getMyy() * 6 + it.getTy());

        try {
            t.inverseTransform2DPoints(srcPts, 3, dstPts, 1, 2);
            if (!is2d) {
                fail("Should have thrown ISE");
            }
            if (!isInvertible) {
                fail("Should have thrown NonInvertibleTransformException");
            }

            assertEquals(1, dstPts[0], 0.00001);
            assertEquals(expected1.getX(), dstPts[1], 0.00001);
            assertEquals(expected1.getY(), dstPts[2], 0.00001);
            assertEquals(expected2.getX(), dstPts[3], 0.00001);
            assertEquals(expected2.getY(), dstPts[4], 0.00001);
            assertEquals(6, dstPts[5], 0.00001);
        } catch (IllegalStateException e) {
            if (is2d) {
                fail("Wrong exception thrown");
            }
        } catch (NonInvertibleTransformException e) {
            if (isInvertible) {
                fail("Wrong exception thrown");
            }
        }
    }

    @Test(expected=NullPointerException.class)
    public void testInverseTransform2DPointsBothPtsNull()
            throws NonInvertibleTransformException {
        t.inverseTransform2DPoints(null, 2, null, 0, 0);
    }

    @Test(expected=NullPointerException.class)
    public void testInverseTransform2DPointsSrcPtsNull()
            throws NonInvertibleTransformException {
        t.inverseTransform2DPoints(null, 2, new double[] { 1, 2, 3 }, 0, 0);
    }

    @Test(expected=NullPointerException.class)
    public void testInverseTransform2DPointsDstPtsNull()
            throws NonInvertibleTransformException {
        t.inverseTransform2DPoints(new double[] { 1, 2, 3, 4 }, 2, null, 0, 0);
    }

    @Test
    public void testInverseTransform2DPointsWithOverlap() throws Exception {
        double[] srcPts = new double[] { 0, 1, 2, 3, 4, 5, 6, 7, 8 };

        Point2D expected1 = new Point2D(
            it.getMxx() * 2 + it.getMxy() * 3 + it.getTx(),
            it.getMyx() * 2 + it.getMyy() * 3 + it.getTy());

        Point2D expected2 = new Point2D(
            it.getMxx() * 4 + it.getMxy() * 5 + it.getTx(),
            it.getMyx() * 4 + it.getMyy() * 5 + it.getTy());

        try {
            t.inverseTransform2DPoints(srcPts, 2, srcPts, 4, 2);
            if (!is2d) {
                fail("Should have thrown ISE");
            }
            if (!isInvertible) {
                fail("Should have thrown NonInvertibleTransformException");
            }
            assertEquals(0, srcPts[0], 0.00001);
            assertEquals(1, srcPts[1], 0.00001);
            assertEquals(2, srcPts[2], 0.00001);
            assertEquals(3, srcPts[3], 0.00001);
            assertEquals(expected1.getX(), srcPts[4], 0.00001);
            assertEquals(expected1.getY(), srcPts[5], 0.00001);
            assertEquals(expected2.getX(), srcPts[6], 0.00001);
            assertEquals(expected2.getY(), srcPts[7], 0.00001);
            assertEquals(8, srcPts[8], 0.00001);
        } catch (IllegalStateException e) {
            if (is2d) {
                fail("Wrong exception thrown");
            }
        } catch (NonInvertibleTransformException e) {
            if (isInvertible) {
                fail("Wrong exception thrown");
            }
        }
    }

    @Test(expected=IndexOutOfBoundsException.class)
    public void testInverseTransform2DPointsSrcOut() throws Exception {
        double[] srcPts = new double[] { 0, 1, 2, 3, 4, 5, 6, 7 };
        double[] dstPts = new double[] { 1, 2, 3, 4, 5, 6 };

        try {
            t.inverseTransform2DPoints(srcPts, 3, dstPts, 0, 3);
        } catch (IllegalStateException e) {
            if (!is2d) {
                throw new IndexOutOfBoundsException("expected result");
            }
        } catch (NonInvertibleTransformException e) {
            if (!isInvertible) {
                throw new IndexOutOfBoundsException("expected result");
            }
        }
    }

    @Test(expected=IndexOutOfBoundsException.class)
    public void testInverseTransform2DPointsDstOut() throws Exception {
        double[] srcPts = new double[] { 0, 1, 2, 3, 4, 5, 6, 7 };
        double[] dstPts = new double[] { 1 };

        try {
            t.inverseTransform2DPoints(srcPts, 1, dstPts, 0, 2);
        } catch (IllegalStateException e) {
            if (!is2d) {
                throw new IndexOutOfBoundsException("expected result");
            }
        } catch (NonInvertibleTransformException e) {
            if (!isInvertible) {
                throw new IndexOutOfBoundsException("expected result");
            }
        }
    }

    @Test
    public void testInverseTransform3DPoints() throws Exception {
        double[] srcPts = new double[] { 0, 1, 2, 3, 4, 5, 6, 7, 8 };
        double[] dstPts = new double[] { 1, 2, 3, 4, 5, 6, 7, 8 };

        Point3D expected1 = new Point3D(
            it.getMxx() * 3 + it.getMxy() * 4 + it.getMxz() * 5 + it.getTx(),
            it.getMyx() * 3 + it.getMyy() * 4 + it.getMyz() * 5 + it.getTy(),
            it.getMzx() * 3 + it.getMzy() * 4 + it.getMzz() * 5 + it.getTz());

        Point3D expected2 = new Point3D(
            it.getMxx() * 6 + it.getMxy() * 7 + it.getMxz() * 8 + it.getTx(),
            it.getMyx() * 6 + it.getMyy() * 7 + it.getMyz() * 8 + it.getTy(),
            it.getMzx() * 6 + it.getMzy() * 7 + it.getMzz() * 8 + it.getTz());

        try {
            t.inverseTransform3DPoints(srcPts, 3, dstPts, 1, 2);
            if (!isInvertible) {
                fail("Should have thrown NonInvertibleTransformException");
            }

            assertEquals(1, dstPts[0], 0.00001);
            assertEquals(expected1.getX(), dstPts[1], 0.00001);
            assertEquals(expected1.getY(), dstPts[2], 0.00001);
            assertEquals(expected1.getZ(), dstPts[3], 0.00001);
            assertEquals(expected2.getX(), dstPts[4], 0.00001);
            assertEquals(expected2.getY(), dstPts[5], 0.00001);
            assertEquals(expected2.getZ(), dstPts[6], 0.00001);
            assertEquals(8, dstPts[7], 0.00001);
        } catch (NonInvertibleTransformException e) {
            if (isInvertible) {
                fail("Wrong exception thrown");
            }
        }
    }

    @Test(expected=NullPointerException.class)
    public void testInverseTransform3DPointsBothPtsNull()
            throws NonInvertibleTransformException {
        t.inverseTransform3DPoints(null, 2, null, 0, 0);
    }

    @Test(expected=NullPointerException.class)
    public void testInverseTransform3DPointsSrcPtsNull()
            throws NonInvertibleTransformException {
        t.inverseTransform3DPoints(null, 2, new double[] { 1, 2, 3 }, 0, 0);
    }

    @Test(expected=NullPointerException.class)
    public void testInverseTransform3DPointsDstPtsNull()
            throws NonInvertibleTransformException {
        t.inverseTransform3DPoints(new double[] { 1, 2, 3, 4 }, 2, null, 0, 0);
    }

    @Test
    public void testInverseTransform3DPointsWithOverlap() throws Exception {
        double[] srcPts = new double[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };

        Point3D expected1 = new Point3D(
            it.getMxx() * 2 + it.getMxy() * 3 + it.getMxz() * 4 + it.getTx(),
            it.getMyx() * 2 + it.getMyy() * 3 + it.getMyz() * 4 + it.getTy(),
            it.getMzx() * 2 + it.getMzy() * 3 + it.getMzz() * 4 + it.getTz());

        Point3D expected2 = new Point3D(
            it.getMxx() * 5 + it.getMxy() * 6 + it.getMxz() * 7 + it.getTx(),
            it.getMyx() * 5 + it.getMyy() * 6 + it.getMyz() * 7 + it.getTy(),
            it.getMzx() * 5 + it.getMzy() * 6 + it.getMzz() * 7 + it.getTz());

        try {
            t.inverseTransform3DPoints(srcPts, 2, srcPts, 3, 2);
            if (!isInvertible) {
                fail("Should have thrown NonInvertibleTransformException");
            }

            assertEquals(0, srcPts[0], 0.00001);
            assertEquals(1, srcPts[1], 0.00001);
            assertEquals(2, srcPts[2], 0.00001);
            assertEquals(expected1.getX(), srcPts[3], 0.00001);
            assertEquals(expected1.getY(), srcPts[4], 0.00001);
            assertEquals(expected1.getZ(), srcPts[5], 0.00001);
            assertEquals(expected2.getX(), srcPts[6], 0.00001);
            assertEquals(expected2.getY(), srcPts[7], 0.00001);
            assertEquals(expected2.getZ(), srcPts[8], 0.00001);
            assertEquals(9, srcPts[9], 0.00001);
        } catch (NonInvertibleTransformException e) {
            if (isInvertible) {
                fail("Wrong exception thrown");
            }
        }
    }

    @Test(expected=IndexOutOfBoundsException.class)
    public void testInverseTransform3DPointsSrcOut() throws Exception {
        double[] srcPts = new double[] { 0, 1, 2, 3, 4, 5, 6, 7 };
        double[] dstPts = new double[] { 1, 2, 3, 4, 5, 6 };

        try {
            t.inverseTransform3DPoints(srcPts, 6, dstPts, 0, 1);
        } catch (NonInvertibleTransformException e) {
            if (!isInvertible) {
                throw new IndexOutOfBoundsException("expected result");
            }
        }
    }

    @Test(expected=IndexOutOfBoundsException.class)
    public void testInverseTransform3DPointsDstOut() throws Exception {
        double[] srcPts = new double[] { 0, 1, 2, 3, 4, 5, 6, 7 };
        double[] dstPts = new double[] { 1 };

        try {
            t.inverseTransform3DPoints(srcPts, 1, dstPts, 0, 1);
        } catch (NonInvertibleTransformException e) {
            if (!isInvertible) {
                throw new IndexOutOfBoundsException("expected result");
            }
        }
    }

    @Test
    public void testDeterminant() {
        assertEquals(TransformHelper.determinant(t), t.determinant(), 0.00001);
    }

    @Test
    public void testIsType2D() {
        Transform clone = t.clone();

        if (is2d) {
            assertTrue(t.isType2D());
            if (TransformHelper.make3D(clone)) {
                assertFalse(clone.isType2D());
            }
        } else {
            assertFalse(t.isType2D());
            if (TransformHelper.make2D(clone)) {
                assertTrue(clone.isType2D());
            }
        }
    }

    @Test
    public void testType2DProperty() {
        Transform clone = t.clone();

        assertEquals("type2D", clone.type2DProperty().getName());
        assertSame(clone, clone.type2DProperty().getBean());
    }

    @Test
    public void testType2DPropertyGetter() {
        Transform clone = t.clone();

        if (is2d) {
            assertTrue(clone.type2DProperty().get());
            assertTrue(clone.isType2D());
            if (TransformHelper.make3D(clone)) {
                assertFalse(clone.type2DProperty().get());
                assertFalse(clone.isType2D());
            }
        } else {
            assertFalse(clone.type2DProperty().get());
            assertFalse(clone.isType2D());
            if (TransformHelper.make2D(clone)) {
                assertTrue(clone.type2DProperty().get());
                assertTrue(clone.isType2D());
            }
        }
    }

    @Test
    public void testType2DPropertyInvalidation() {
        final Transform clone = t.clone();

        InvalidationListener l =
                valueModel -> {
                     if (is2d) {
                         assertFalse(clone.type2DProperty().get());
                     } else {
                         assertTrue(clone.type2DProperty().get());
                     }
                     listenerCalled = true;
                };

        clone.type2DProperty().addListener(l);

        listenerCalled = false;

        if (is2d) {
            if (TransformHelper.make3D(clone)) {
                assertTrue(listenerCalled);
            }
        } else {
            if (TransformHelper.make2D(clone)) {
                assertTrue(listenerCalled);
            }
        }

        listenerCalled = false;
        clone.type2DProperty().removeListener(l);

        if (is2d) {
            TransformHelper.make2D(clone);
            assertFalse(listenerCalled);
        } else {
            TransformHelper.make3D(clone);
            assertFalse(listenerCalled);
        }
    }

    @Test
    public void testType2DPropertyChange() {
        final Transform clone = t.clone();

        ChangeListener<Boolean> l =
                (observable, oldValue, newValue) -> {

                    if ((is2d && (eventCounter == 0 || eventCounter == 2))
                            || (!is2d && eventCounter == 1)) {
                        assertTrue(oldValue);
                        assertFalse(newValue);
                        assertFalse(clone.type2DProperty().get());
                    } else {
                        assertFalse(oldValue);
                        assertTrue(newValue);
                        assertTrue(clone.type2DProperty().get());
                    }

                    listenerCalled = true;
                    eventCounter++;
                };

        clone.type2DProperty().addListener(l);

        listenerCalled = false;
        eventCounter = 0;
        TransformHelper.modify(clone, 42);
        assertFalse(listenerCalled);

        if (is2d) {
            if (TransformHelper.make3D(clone)) {
                assertTrue(listenerCalled);
                listenerCalled = false;
                TransformHelper.make3D(clone);
                assertFalse(listenerCalled);
                TransformHelper.make2D(clone);
                assertTrue(listenerCalled);
                listenerCalled = false;
                clone.type2DProperty().removeListener(l);
                TransformHelper.make3D(clone);
                assertFalse(listenerCalled);
            }
        } else {
            if (TransformHelper.make2D(clone)) {
                assertTrue(listenerCalled);
                listenerCalled = false;
                TransformHelper.make2D(clone);
                assertFalse(listenerCalled);
                TransformHelper.make3D(clone);
                assertTrue(listenerCalled);
                listenerCalled = false;
                clone.type2DProperty().removeListener(l);
                TransformHelper.make2D(clone);
                assertFalse(listenerCalled);
            }
        }
    }

    @Test
    public void testIsIdentity() {
        Transform clone = t.clone();

        if (isIdentity) {
            assertTrue(t.isIdentity());
            if (TransformHelper.modify(clone, 42)) {
                assertFalse(clone.isIdentity());
            }
        } else {
            assertFalse(t.isIdentity());
            if (TransformHelper.makeIdentity(clone)) {
                assertTrue(clone.isIdentity());
            }
        }
    }

    @Test
    public void testIdentityProperty() {
        Transform clone = t.clone();

        assertEquals("identity", clone.identityProperty().getName());
        assertSame(clone, clone.identityProperty().getBean());
    }

    @Test
    public void testIdentityPropertyGetter() {
        Transform clone = t.clone();

        if (isIdentity) {
            assertTrue(clone.identityProperty().get());
            assertTrue(clone.isIdentity());
            if (TransformHelper.modify(clone, 42)) {
                assertFalse(clone.identityProperty().get());
                assertFalse(clone.isIdentity());
            }
        } else {
            assertFalse(clone.identityProperty().get());
            assertFalse(clone.isIdentity());
            if (TransformHelper.makeIdentity(clone)) {
                assertTrue(clone.identityProperty().get());
                assertTrue(clone.isIdentity());
            }
        }
    }

    @Test
    public void testIdentityPropertyInvalidation() {
        final Transform clone = t.clone();

        InvalidationListener l =
                valueModel -> {
                     if (isIdentity) {
                         if (!clone.identityProperty().get()) {
                            listenerCalled = true;
                         }
                     } else {
                         if (clone.identityProperty().get()) {
                            listenerCalled = true;
                         }
                     }
                };

        clone.identityProperty().addListener(l);

        listenerCalled = false;

        if (isIdentity) {
            if (TransformHelper.modify(clone, 42)) {
                assertTrue(listenerCalled);
            }
        } else {
            if (TransformHelper.makeIdentity(clone)) {
                assertTrue(listenerCalled);
            }
        }

        listenerCalled = false;
        clone.identityProperty().removeListener(l);

        if (isIdentity) {
            TransformHelper.makeIdentity(clone);
            TransformHelper.modify(clone, 42);
            assertFalse(listenerCalled);
        } else {
            TransformHelper.modify(clone, 42);
            TransformHelper.makeIdentity(clone);
            assertFalse(listenerCalled);
        }
    }

    @Test
    public void testIdentityPropertyChange() {
        final Transform clone = t.clone();

        ChangeListener<Boolean> l =
                (observable, oldValue, newValue) -> {
                    if (isIdentity) {
                        if (oldValue == true && newValue == false
                                && clone.identityProperty().get() == false) {
                            listenerCalled = true;
                        }
                    } else {
                        if (oldValue == false && newValue == true
                                && clone.identityProperty().get() == true) {
                            listenerCalled = true;
                        }
                    }

                };

        clone.identityProperty().addListener(l);

        listenerCalled = false;

        if (isIdentity) {
            if (TransformHelper.modify(clone, 42)) {
                assertTrue(listenerCalled);
                listenerCalled = false;
                TransformHelper.modify(clone, 43);
                assertFalse(listenerCalled);

                clone.identityProperty().removeListener(l);
                TransformHelper.makeIdentity(clone);
                TransformHelper.modify(clone, 42);
                assertFalse(listenerCalled);
            }
        } else {
            TransformHelper.modify(clone, 42);
            assertFalse(listenerCalled);
            if (TransformHelper.makeIdentity(clone)) {
                assertTrue(listenerCalled);
                listenerCalled = false;
                TransformHelper.makeIdentity(clone);
                assertFalse(listenerCalled);

                clone.identityProperty().removeListener(l);
                TransformHelper.modify(clone, 42);
                TransformHelper.makeIdentity(clone);
                assertFalse(listenerCalled);
            }
        }
    }

    @Test
    public void testSimilarTo() {
        Transform clone = t.clone();

        assertTrue(t.similarTo(clone, new BoundingBox(-10000, -10000, 10000, 10000), 1e-10));

        if (TransformHelper.tinyModify(clone)) {
            assertTrue(t.similarTo(clone, new BoundingBox(0, 0, 1, 1), 5));
            if (is2d) {
                assertTrue(t.similarTo(clone, new BoundingBox(0, 0, 0, 0.1, 0.1, 10000), 2));
            } else {
                assertFalse(t.similarTo(clone, new BoundingBox(0, 0, 0, 0.1, 0.1, 10000), 2));
            }
            assertFalse(t.similarTo(clone, new BoundingBox(0, 0, 1000, 1000), 0.5));

            if (t instanceof Translate) {
                assertTrue(t.similarTo(clone, new BoundingBox(0, 0, 1, 1), 4));
                assertFalse(t.similarTo(clone, new BoundingBox(0, 0, 1, 1), 0.5));
            }

            if (t instanceof Scale) {
                assertTrue(t.similarTo(clone, new BoundingBox(0, 0, 1, 1), 5));
                assertFalse(t.similarTo(clone, new BoundingBox(0, 0, 1000, 1000), 5));
            }

        }
    }

    @Test(expected=NullPointerException.class)
    public void testSimilarToNullTransform() {
        t.similarTo(null, new BoundingBox(0, 0, 0, 1, 1, 1), 0);
    }

    @Test(expected=NullPointerException.class)
    public void testSimilarToNullRange() {
        t.similarTo(t, null, 0);
    }

    private void assertGetElement(MatrixType type, int row, int col,
            double expected, boolean iae, boolean iob) {
        double res = Double.MIN_VALUE;

        try {
            res = t.getElement(type, row, col);
        } catch (IllegalArgumentException e) {
            if (iae) {
                // ok
                return;
            }
        } catch (IndexOutOfBoundsException e) {
            if (iob) {
                // ok
                return;
            }
        }

        if (iae) {
            fail("Should have thrown IAE");
        }
        if (iob) {
            fail("Should have thrown IOB");
        }

        assertEquals(expected, res, 1e-100);
    }

    @Test
    public void testGetElement() {
        assertGetElement(MatrixType.MT_2D_2x3, 0, 0, t.getMxx(), !is2d, false);
        assertGetElement(MatrixType.MT_2D_2x3, 0, 1, t.getMxy(), !is2d, false);
        assertGetElement(MatrixType.MT_2D_2x3, 0, 2, t.getTx(), !is2d, false);
        assertGetElement(MatrixType.MT_2D_2x3, 1, 0, t.getMyx(), !is2d, false);
        assertGetElement(MatrixType.MT_2D_2x3, 1, 1, t.getMyy(), !is2d, false);
        assertGetElement(MatrixType.MT_2D_2x3, 1, 2, t.getTy(), !is2d, false);
        assertGetElement(MatrixType.MT_2D_2x3, -1, 0, 0, !is2d, true);
        assertGetElement(MatrixType.MT_2D_2x3, 2, 1, 0, !is2d, true);
        assertGetElement(MatrixType.MT_2D_2x3, 1, 3, 0, !is2d, true);
        assertGetElement(MatrixType.MT_2D_2x3, 1, -1, 0, !is2d, true);
        assertGetElement(MatrixType.MT_2D_3x3, 0, 0, t.getMxx(), !is2d, false);
        assertGetElement(MatrixType.MT_2D_3x3, 0, 1, t.getMxy(), !is2d, false);
        assertGetElement(MatrixType.MT_2D_3x3, 0, 2, t.getTx(), !is2d, false);
        assertGetElement(MatrixType.MT_2D_3x3, 1, 0, t.getMyx(), !is2d, false);
        assertGetElement(MatrixType.MT_2D_3x3, 1, 1, t.getMyy(), !is2d, false);
        assertGetElement(MatrixType.MT_2D_3x3, 1, 2, t.getTy(), !is2d, false);
        assertGetElement(MatrixType.MT_2D_3x3, 2, 0, 0, !is2d, false);
        assertGetElement(MatrixType.MT_2D_3x3, 2, 1, 0, !is2d, false);
        assertGetElement(MatrixType.MT_2D_3x3, 2, 2, 1, !is2d, false);
        assertGetElement(MatrixType.MT_2D_3x3, -1, 0, 0, !is2d, true);
        assertGetElement(MatrixType.MT_2D_3x3, 3, 1, 0, !is2d, true);
        assertGetElement(MatrixType.MT_2D_3x3, 1, 3, 0, !is2d, true);
        assertGetElement(MatrixType.MT_2D_3x3, 1, -1, 0, !is2d, true);
        assertGetElement(MatrixType.MT_3D_3x4, 0, 0, t.getMxx(), false, false);
        assertGetElement(MatrixType.MT_3D_3x4, 0, 1, t.getMxy(), false, false);
        assertGetElement(MatrixType.MT_3D_3x4, 0, 2, t.getMxz(), false, false);
        assertGetElement(MatrixType.MT_3D_3x4, 0, 3, t.getTx(), false, false);
        assertGetElement(MatrixType.MT_3D_3x4, 1, 0, t.getMyx(), false, false);
        assertGetElement(MatrixType.MT_3D_3x4, 1, 1, t.getMyy(), false, false);
        assertGetElement(MatrixType.MT_3D_3x4, 1, 2, t.getMyz(), false, false);
        assertGetElement(MatrixType.MT_3D_3x4, 1, 3, t.getTy(), false, false);
        assertGetElement(MatrixType.MT_3D_3x4, 2, 0, t.getMzx(), false, false);
        assertGetElement(MatrixType.MT_3D_3x4, 2, 1, t.getMzy(), false, false);
        assertGetElement(MatrixType.MT_3D_3x4, 2, 2, t.getMzz(), false, false);
        assertGetElement(MatrixType.MT_3D_3x4, 2, 3, t.getTz(), false, false);
        assertGetElement(MatrixType.MT_3D_3x4, -1, 0, 0, false, true);
        assertGetElement(MatrixType.MT_3D_3x4, 3, 1, 0, false, true);
        assertGetElement(MatrixType.MT_3D_3x4, 1, 4, 0, false, true);
        assertGetElement(MatrixType.MT_3D_3x4, 1, -1, 0, false, true);
        assertGetElement(MatrixType.MT_3D_4x4, 0, 0, t.getMxx(), false, false);
        assertGetElement(MatrixType.MT_3D_4x4, 0, 1, t.getMxy(), false, false);
        assertGetElement(MatrixType.MT_3D_4x4, 0, 2, t.getMxz(), false, false);
        assertGetElement(MatrixType.MT_3D_4x4, 0, 3, t.getTx(), false, false);
        assertGetElement(MatrixType.MT_3D_4x4, 1, 0, t.getMyx(), false, false);
        assertGetElement(MatrixType.MT_3D_4x4, 1, 1, t.getMyy(), false, false);
        assertGetElement(MatrixType.MT_3D_4x4, 1, 2, t.getMyz(), false, false);
        assertGetElement(MatrixType.MT_3D_4x4, 1, 3, t.getTy(), false, false);
        assertGetElement(MatrixType.MT_3D_4x4, 2, 0, t.getMzx(), false, false);
        assertGetElement(MatrixType.MT_3D_4x4, 2, 1, t.getMzy(), false, false);
        assertGetElement(MatrixType.MT_3D_4x4, 2, 2, t.getMzz(), false, false);
        assertGetElement(MatrixType.MT_3D_4x4, 2, 3, t.getTz(), false, false);
        assertGetElement(MatrixType.MT_3D_4x4, 3, 0, 0, false, false);
        assertGetElement(MatrixType.MT_3D_4x4, 3, 1, 0, false, false);
        assertGetElement(MatrixType.MT_3D_4x4, 3, 2, 0, false, false);
        assertGetElement(MatrixType.MT_3D_4x4, 3, 3, 1, false, false);
        assertGetElement(MatrixType.MT_3D_4x4, -1, 0, 0, false, true);
        assertGetElement(MatrixType.MT_3D_4x4, 4, 1, 0, false, true);
        assertGetElement(MatrixType.MT_3D_4x4, 1, 4, 0, false, true);
        assertGetElement(MatrixType.MT_3D_4x4, 1, -1, 0, false, true);
    }

    @Test(expected=NullPointerException.class)
    public void testGetElementNullType() {
        t.getElement(null, 0, 0);
    }

    private void assertArray(MatrixType type, double[] a, Transform t) {
        switch (type) {
            case MT_2D_3x3:
                assertEquals(0, a[6], 1e-100);
                assertEquals(0, a[7], 1e-100);
                assertEquals(1, a[8], 1e-100);
                // fallthrough
            case MT_2D_2x3:
                assertEquals(t.getMxx(), a[0], 1e-100);
                assertEquals(t.getMxy(), a[1], 1e-100);
                assertEquals(t.getTx(),  a[2], 1e-100);
                assertEquals(t.getMyx(), a[3], 1e-100);
                assertEquals(t.getMyy(), a[4], 1e-100);
                assertEquals(t.getTy(),  a[5], 1e-100);
                break;
            case MT_3D_4x4:
                assertEquals(0, a[12], 1e-100);
                assertEquals(0, a[13], 1e-100);
                assertEquals(0, a[14], 1e-100);
                assertEquals(1, a[15], 1e-100);
                // fallthrough
            case MT_3D_3x4:
                assertEquals(t.getMxx(), a[0], 1e-100);
                assertEquals(t.getMxy(), a[1], 1e-100);
                assertEquals(t.getMxz(), a[2], 1e-100);
                assertEquals(t.getTx(),  a[3], 1e-100);
                assertEquals(t.getMyx(), a[4], 1e-100);
                assertEquals(t.getMyy(), a[5], 1e-100);
                assertEquals(t.getMyz(), a[6], 1e-100);
                assertEquals(t.getTy(),  a[7], 1e-100);
                assertEquals(t.getMzx(), a[8], 1e-100);
                assertEquals(t.getMzy(), a[9], 1e-100);
                assertEquals(t.getMzz(), a[10], 1e-100);
                assertEquals(t.getTz(),  a[11], 1e-100);
                break;
        }
    }

    private void assertToArray2D(MatrixType type, double[] tmp,
            boolean shouldPass, boolean shouldUse) {
        double[] a = null;
        try {
            if (shouldPass) {
                a = t.toArray(type, tmp);
            } else {
                a = t.toArray(type);
            }
        } catch (IllegalArgumentException e) {
            if (is2d) {
                fail("Wrong exception thrown");
            }
            return;
        }

        if (!is2d) {
            fail("Should have thrown IAE");
        } else {
            assertNotNull(a);
            if (shouldUse) {
                assertSame(tmp, a);
            }
            assertArray(type, a, t);
        }
    }

    private void assertToArray3D(MatrixType type, double[] tmp,
            boolean shouldPass, boolean shouldUse) {
        double[] a = null;

        if (shouldPass) {
            a = t.toArray(type, tmp);
        } else {
            a = t.toArray(type);
        }

        assertNotNull(a);
        if (shouldUse) {
            assertSame(tmp, a);
        }
        assertArray(type, a, t);
    }

    @Test
    public void testToArray() {

        assertToArray2D(MatrixType.MT_2D_2x3, null, false, false);
        assertToArray2D(MatrixType.MT_2D_2x3, null, true, false);
        assertToArray2D(MatrixType.MT_2D_2x3, new double[4], true, false);
        assertToArray2D(MatrixType.MT_2D_2x3, new double[6], true, true);

        assertToArray2D(MatrixType.MT_2D_3x3, null, false, false);
        assertToArray2D(MatrixType.MT_2D_3x3, null, true, false);
        assertToArray2D(MatrixType.MT_2D_3x3, new double[8], true, false);
        assertToArray2D(MatrixType.MT_2D_3x3, new double[9], true, true);

        assertToArray3D(MatrixType.MT_3D_3x4, null, false, false);
        assertToArray3D(MatrixType.MT_3D_3x4, null, true, false);
        assertToArray3D(MatrixType.MT_3D_3x4, new double[11], true, false);
        assertToArray3D(MatrixType.MT_3D_3x4, new double[12], true, true);

        assertToArray3D(MatrixType.MT_3D_4x4, null, false, false);
        assertToArray3D(MatrixType.MT_3D_4x4, null, true, false);
        assertToArray3D(MatrixType.MT_3D_4x4, new double[15], true, false);
        assertToArray3D(MatrixType.MT_3D_4x4, new double[16], true, true);
    }

    @Test(expected=NullPointerException.class)
    public void testToArrayNullType1() {
        t.toArray(null);
    }

    @Test(expected=NullPointerException.class)
    public void testToArrayNullType2() {
        t.toArray(null, new double[] {});
    }

    private void assertRow(MatrixType type, int row, double[] a, Transform t) {
        switch (type) {
            case MT_2D_3x3:
                if (row == 2) {
                    assertEquals(0, a[0], 1e-100);
                    assertEquals(0, a[1], 1e-100);
                    assertEquals(1, a[2], 1e-100);
                    return;
                }
                // fallthrough
            case MT_2D_2x3:
                if (row == 0) {
                    assertEquals(t.getMxx(), a[0], 1e-100);
                    assertEquals(t.getMxy(), a[1], 1e-100);
                    assertEquals(t.getTx(),  a[2], 1e-100);
                    return;
                } else if (row == 1) {
                    assertEquals(t.getMyx(), a[0], 1e-100);
                    assertEquals(t.getMyy(), a[1], 1e-100);
                    assertEquals(t.getTy(),  a[2], 1e-100);
                    return;
                }
                break;
            case MT_3D_4x4:
                if (row == 3) {
                    assertEquals(0, a[0], 1e-100);
                    assertEquals(0, a[1], 1e-100);
                    assertEquals(0, a[2], 1e-100);
                    assertEquals(1, a[3], 1e-100);
                    return;
                }
                // fallthrough
            case MT_3D_3x4:
                if (row == 0) {
                    assertEquals(t.getMxx(), a[0], 1e-100);
                    assertEquals(t.getMxy(), a[1], 1e-100);
                    assertEquals(t.getMxz(), a[2], 1e-100);
                    assertEquals(t.getTx(),  a[3], 1e-100);
                    return;
                } else if (row == 1) {
                    assertEquals(t.getMyx(), a[0], 1e-100);
                    assertEquals(t.getMyy(), a[1], 1e-100);
                    assertEquals(t.getMyz(), a[2], 1e-100);
                    assertEquals(t.getTy(),  a[3], 1e-100);
                    return;
                } else if (row == 2) {
                    assertEquals(t.getMzx(), a[0], 1e-100);
                    assertEquals(t.getMzy(), a[1], 1e-100);
                    assertEquals(t.getMzz(), a[2], 1e-100);
                    assertEquals(t.getTz(),  a[3], 1e-100);
                    return;
                }
                break;
        }

        fail("Should have thrown IOB");
    }

    private void assertRow2D(MatrixType type, int row, double[] tmp,
            boolean shouldPass, boolean shouldUse, boolean iob) {
        double[] a = null;
        try {
            if (shouldPass) {
                a = t.row(type, row, tmp);
            } else {
                a = t.row(type, row);
            }
        } catch (IllegalArgumentException e) {
            if (is2d) {
                fail("Wrong exception thrown");
            }
            return;
        } catch (IndexOutOfBoundsException e) {
            if (!iob) {
                fail("Wrong exception thrown");
            }
            return;
        }
        if (!is2d) {
            fail("Should have thrown IAE");
        } else if (iob) {
            fail("Should have thrown IOB");
        } else {
            assertNotNull(a);
            if (shouldUse) {
                assertSame(tmp, a);
            }
            assertRow(type, row, a, t);
        }
    }

    private void assertRow3D(MatrixType type, int row, double[] tmp,
            boolean shouldPass, boolean shouldUse, boolean iob) {
        double[] a = null;
        try {
            if (shouldPass) {
                a = t.row(type, row, tmp);
            } else {
                a = t.row(type, row);
            }
        } catch (IndexOutOfBoundsException e) {
            if (!iob) {
                fail("Wrong exception thrown");
            }
            return;
        }
        if (iob) {
            fail("Should have thrown IOB");
        } else {
            assertNotNull(a);
            if (shouldUse) {
                assertSame(tmp, a);
            }
            assertRow(type, row, a, t);
        }
    }

    @Test
    public void testRow() {
        assertRow2D(MatrixType.MT_2D_2x3, 0, null, false, false, false);
        assertRow2D(MatrixType.MT_2D_2x3, 0, null, true, false, false);
        assertRow2D(MatrixType.MT_2D_2x3, 0, new double[2], true, false, false);
        assertRow2D(MatrixType.MT_2D_2x3, 0, new double[3], true, true, false);
        assertRow2D(MatrixType.MT_2D_2x3, 1, null, false, false, false);
        assertRow2D(MatrixType.MT_2D_2x3, 1, null, true, false, false);
        assertRow2D(MatrixType.MT_2D_2x3, 1, new double[2], true, false, false);
        assertRow2D(MatrixType.MT_2D_2x3, 1, new double[3], true, true, false);
        assertRow2D(MatrixType.MT_2D_2x3, -1, null, true, false, true);
        assertRow2D(MatrixType.MT_2D_2x3, 2, null, false, false, true);

        assertRow2D(MatrixType.MT_2D_3x3, 0, null, false, false, false);
        assertRow2D(MatrixType.MT_2D_3x3, 0, null, true, false, false);
        assertRow2D(MatrixType.MT_2D_3x3, 0, new double[2], true, false, false);
        assertRow2D(MatrixType.MT_2D_3x3, 0, new double[3], true, true, false);
        assertRow2D(MatrixType.MT_2D_3x3, 1, null, false, false, false);
        assertRow2D(MatrixType.MT_2D_3x3, 1, null, true, false, false);
        assertRow2D(MatrixType.MT_2D_3x3, 1, new double[2], true, false, false);
        assertRow2D(MatrixType.MT_2D_3x3, 1, new double[3], true, true, false);
        assertRow2D(MatrixType.MT_2D_3x3, 2, null, false, false, false);
        assertRow2D(MatrixType.MT_2D_3x3, 2, null, true, false, false);
        assertRow2D(MatrixType.MT_2D_3x3, 2, new double[2], true, false, false);
        assertRow2D(MatrixType.MT_2D_3x3, 2, new double[3], true, true, false);
        assertRow2D(MatrixType.MT_2D_3x3, -1, null, true, false, true);
        assertRow2D(MatrixType.MT_2D_3x3, 3, null, false, false, true);

        assertRow3D(MatrixType.MT_3D_3x4, 0, null, false, false, false);
        assertRow3D(MatrixType.MT_3D_3x4, 0, null, true, false, false);
        assertRow3D(MatrixType.MT_3D_3x4, 0, new double[3], true, false, false);
        assertRow3D(MatrixType.MT_3D_3x4, 0, new double[4], true, true, false);
        assertRow3D(MatrixType.MT_3D_3x4, 1, null, false, false, false);
        assertRow3D(MatrixType.MT_3D_3x4, 1, null, true, false, false);
        assertRow3D(MatrixType.MT_3D_3x4, 1, new double[3], true, false, false);
        assertRow3D(MatrixType.MT_3D_3x4, 1, new double[4], true, true, false);
        assertRow3D(MatrixType.MT_3D_3x4, 2, null, false, false, false);
        assertRow3D(MatrixType.MT_3D_3x4, 2, null, true, false, false);
        assertRow3D(MatrixType.MT_3D_3x4, 2, new double[3], true, false, false);
        assertRow3D(MatrixType.MT_3D_3x4, 2, new double[4], true, true, false);
        assertRow3D(MatrixType.MT_3D_3x4, -1, null, true, false, true);
        assertRow3D(MatrixType.MT_3D_3x4, 3, null, false, false, true);

        assertRow3D(MatrixType.MT_3D_4x4, 0, null, false, false, false);
        assertRow3D(MatrixType.MT_3D_4x4, 0, null, true, false, false);
        assertRow3D(MatrixType.MT_3D_4x4, 0, new double[3], true, false, false);
        assertRow3D(MatrixType.MT_3D_4x4, 0, new double[4], true, true, false);
        assertRow3D(MatrixType.MT_3D_4x4, 1, null, false, false, false);
        assertRow3D(MatrixType.MT_3D_4x4, 1, null, true, false, false);
        assertRow3D(MatrixType.MT_3D_4x4, 1, new double[3], true, false, false);
        assertRow3D(MatrixType.MT_3D_4x4, 1, new double[4], true, true, false);
        assertRow3D(MatrixType.MT_3D_4x4, 2, null, false, false, false);
        assertRow3D(MatrixType.MT_3D_4x4, 2, null, true, false, false);
        assertRow3D(MatrixType.MT_3D_4x4, 2, new double[3], true, false, false);
        assertRow3D(MatrixType.MT_3D_4x4, 2, new double[4], true, true, false);
        assertRow3D(MatrixType.MT_3D_4x4, 3, null, false, false, false);
        assertRow3D(MatrixType.MT_3D_4x4, 3, null, true, false, false);
        assertRow3D(MatrixType.MT_3D_4x4, 3, new double[3], true, false, false);
        assertRow3D(MatrixType.MT_3D_4x4, 3, new double[4], true, true, false);
        assertRow3D(MatrixType.MT_3D_4x4, -1, null, true, false, true);
        assertRow3D(MatrixType.MT_3D_4x4, 4, null, false, false, true);
    }

    @Test(expected=NullPointerException.class)
    public void testRowNullType1() {
        t.row(null, 0);
    }

    @Test(expected=NullPointerException.class)
    public void testRowNullType2() {
        t.row(null, 0, new double[] {});
    }

    private void assertCol(MatrixType type, int col, double[] a, Transform t) {
        switch (type) {
            case MT_2D_2x3:
                if (col == 0) {
                    assertEquals(t.getMxx(), a[0], 1e-100);
                    assertEquals(t.getMyx(), a[1], 1e-100);
                    return;
                } else if (col == 1) {
                    assertEquals(t.getMxy(), a[0], 1e-100);
                    assertEquals(t.getMyy(), a[1], 1e-100);
                    return;
                } else if (col == 2) {
                    assertEquals(t.getTx(), a[0], 1e-100);
                    assertEquals(t.getTy(), a[1], 1e-100);
                    return;
                }
                break;
            case MT_2D_3x3:
                if (col == 0) {
                    assertEquals(t.getMxx(), a[0], 1e-100);
                    assertEquals(t.getMyx(), a[1], 1e-100);
                    assertEquals(0, a[2], 1e-100);
                    return;
                } else if (col == 1) {
                    assertEquals(t.getMxy(), a[0], 1e-100);
                    assertEquals(t.getMyy(), a[1], 1e-100);
                    assertEquals(0, a[2], 1e-100);
                    return;
                } else if (col == 2) {
                    assertEquals(t.getTx(), a[0], 1e-100);
                    assertEquals(t.getTy(), a[1], 1e-100);
                    assertEquals(1, a[2], 1e-100);
                    return;
                }
                break;
            case MT_3D_3x4:
                if (col == 0) {
                    assertEquals(t.getMxx(), a[0], 1e-100);
                    assertEquals(t.getMyx(), a[1], 1e-100);
                    assertEquals(t.getMzx(), a[2], 1e-100);
                    return;
                } else if (col == 1) {
                    assertEquals(t.getMxy(), a[0], 1e-100);
                    assertEquals(t.getMyy(), a[1], 1e-100);
                    assertEquals(t.getMzy(), a[2], 1e-100);
                    return;
                } else if (col == 2) {
                    assertEquals(t.getMxz(), a[0], 1e-100);
                    assertEquals(t.getMyz(), a[1], 1e-100);
                    assertEquals(t.getMzz(), a[2], 1e-100);
                    return;
                } else if (col == 3) {
                    assertEquals(t.getTx(), a[0], 1e-100);
                    assertEquals(t.getTy(), a[1], 1e-100);
                    assertEquals(t.getTz(), a[2], 1e-100);
                    return;
                }
                break;
            case MT_3D_4x4:
                if (col == 0) {
                    assertEquals(t.getMxx(), a[0], 1e-100);
                    assertEquals(t.getMyx(), a[1], 1e-100);
                    assertEquals(t.getMzx(), a[2], 1e-100);
                    assertEquals(0, a[3], 1e-100);
                    return;
                } else if (col == 1) {
                    assertEquals(t.getMxy(), a[0], 1e-100);
                    assertEquals(t.getMyy(), a[1], 1e-100);
                    assertEquals(t.getMzy(), a[2], 1e-100);
                    assertEquals(0, a[3], 1e-100);
                    return;
                } else if (col == 2) {
                    assertEquals(t.getMxz(), a[0], 1e-100);
                    assertEquals(t.getMyz(), a[1], 1e-100);
                    assertEquals(t.getMzz(), a[2], 1e-100);
                    assertEquals(0, a[3], 1e-100);
                    return;
                } else if (col == 3) {
                    assertEquals(t.getTx(), a[0], 1e-100);
                    assertEquals(t.getTy(), a[1], 1e-100);
                    assertEquals(t.getTz(), a[2], 1e-100);
                    assertEquals(1, a[3], 1e-100);
                    return;
                }
                break;
        }

        fail("Should have thrown IOB");
    }

    private void assertCol2D(MatrixType type, int col, double[] tmp,
            boolean shouldPass, boolean shouldUse, boolean iob) {
        double[] a = null;
        try {
            if (shouldPass) {
                a = t.column(type, col, tmp);
            } else {
                a = t.column(type, col);
            }
        } catch (IllegalArgumentException e) {
            if (is2d) {
                fail("Wrong exception thrown");
            }
            return;
        } catch (IndexOutOfBoundsException e) {
            if (!iob) {
                fail("Wrong exception thrown");
            }
            return;
        }
        if (!is2d) {
            fail("Should have thrown IAE");
        } else if (iob) {
            fail("Should have thrown IOB");
        } else {
            assertNotNull(a);
            if (shouldUse) {
                assertSame(tmp, a);
            }
            assertCol(type, col, a, t);
        }
    }

    private void assertCol3D(MatrixType type, int col, double[] tmp,
            boolean shouldPass, boolean shouldUse, boolean iob) {
        double[] a = null;
        try {
            if (shouldPass) {
                a = t.column(type, col, tmp);
            } else {
                a = t.column(type, col);
            }
        } catch (IndexOutOfBoundsException e) {
            if (!iob) {
                fail("Wrong exception thrown");
            }
            return;
        }
        if (iob) {
            fail("Should have thrown IOB");
        } else {
            assertNotNull(a);
            if (shouldUse) {
                assertSame(tmp, a);
            }
            assertCol(type, col, a, t);
        }
    }

    @Test
    public void testColumn() {
        assertCol2D(MatrixType.MT_2D_2x3, 0, null, false, false, false);
        assertCol2D(MatrixType.MT_2D_2x3, 0, null, true, false, false);
        assertCol2D(MatrixType.MT_2D_2x3, 0, new double[1], true, false, false);
        assertCol2D(MatrixType.MT_2D_2x3, 0, new double[2], true, true, false);
        assertCol2D(MatrixType.MT_2D_2x3, 1, null, false, false, false);
        assertCol2D(MatrixType.MT_2D_2x3, 1, null, true, false, false);
        assertCol2D(MatrixType.MT_2D_2x3, 1, new double[1], true, false, false);
        assertCol2D(MatrixType.MT_2D_2x3, 1, new double[2], true, true, false);
        assertCol2D(MatrixType.MT_2D_2x3, 2, null, false, false, false);
        assertCol2D(MatrixType.MT_2D_2x3, 2, null, true, false, false);
        assertCol2D(MatrixType.MT_2D_2x3, 2, new double[1], true, false, false);
        assertCol2D(MatrixType.MT_2D_2x3, 2, new double[2], true, true, false);
        assertCol2D(MatrixType.MT_2D_2x3, -1, null, true, false, true);
        assertCol2D(MatrixType.MT_2D_2x3, 3, null, false, false, true);

        assertCol2D(MatrixType.MT_2D_3x3, 0, null, false, false, false);
        assertCol2D(MatrixType.MT_2D_3x3, 0, null, true, false, false);
        assertCol2D(MatrixType.MT_2D_3x3, 0, new double[2], true, false, false);
        assertCol2D(MatrixType.MT_2D_3x3, 0, new double[3], true, true, false);
        assertCol2D(MatrixType.MT_2D_3x3, 1, null, false, false, false);
        assertCol2D(MatrixType.MT_2D_3x3, 1, null, true, false, false);
        assertCol2D(MatrixType.MT_2D_3x3, 1, new double[2], true, false, false);
        assertCol2D(MatrixType.MT_2D_3x3, 1, new double[3], true, true, false);
        assertCol2D(MatrixType.MT_2D_3x3, 2, null, false, false, false);
        assertCol2D(MatrixType.MT_2D_3x3, 2, null, true, false, false);
        assertCol2D(MatrixType.MT_2D_3x3, 2, new double[2], true, false, false);
        assertCol2D(MatrixType.MT_2D_3x3, 2, new double[3], true, true, false);
        assertCol2D(MatrixType.MT_2D_3x3, -1, null, true, false, true);
        assertCol2D(MatrixType.MT_2D_3x3, 3, null, false, false, true);

        assertCol3D(MatrixType.MT_3D_3x4, 0, null, false, false, false);
        assertCol3D(MatrixType.MT_3D_3x4, 0, null, true, false, false);
        assertCol3D(MatrixType.MT_3D_3x4, 0, new double[2], true, false, false);
        assertCol3D(MatrixType.MT_3D_3x4, 0, new double[3], true, true, false);
        assertCol3D(MatrixType.MT_3D_3x4, 1, null, false, false, false);
        assertCol3D(MatrixType.MT_3D_3x4, 1, null, true, false, false);
        assertCol3D(MatrixType.MT_3D_3x4, 1, new double[2], true, false, false);
        assertCol3D(MatrixType.MT_3D_3x4, 1, new double[3], true, true, false);
        assertCol3D(MatrixType.MT_3D_3x4, 2, null, false, false, false);
        assertCol3D(MatrixType.MT_3D_3x4, 2, null, true, false, false);
        assertCol3D(MatrixType.MT_3D_3x4, 2, new double[2], true, false, false);
        assertCol3D(MatrixType.MT_3D_3x4, 2, new double[3], true, true, false);
        assertCol3D(MatrixType.MT_3D_3x4, 3, null, false, false, false);
        assertCol3D(MatrixType.MT_3D_3x4, 3, null, true, false, false);
        assertCol3D(MatrixType.MT_3D_3x4, 3, new double[2], true, false, false);
        assertCol3D(MatrixType.MT_3D_3x4, 3, new double[3], true, true, false);
        assertCol3D(MatrixType.MT_3D_3x4, -1, null, true, false, true);
        assertCol3D(MatrixType.MT_3D_3x4, 4, null, false, false, true);

        assertCol3D(MatrixType.MT_3D_4x4, 0, null, false, false, false);
        assertCol3D(MatrixType.MT_3D_4x4, 0, null, true, false, false);
        assertCol3D(MatrixType.MT_3D_4x4, 0, new double[3], true, false, false);
        assertCol3D(MatrixType.MT_3D_4x4, 0, new double[4], true, true, false);
        assertCol3D(MatrixType.MT_3D_4x4, 1, null, false, false, false);
        assertCol3D(MatrixType.MT_3D_4x4, 1, null, true, false, false);
        assertCol3D(MatrixType.MT_3D_4x4, 1, new double[3], true, false, false);
        assertCol3D(MatrixType.MT_3D_4x4, 1, new double[4], true, true, false);
        assertCol3D(MatrixType.MT_3D_4x4, 2, null, false, false, false);
        assertCol3D(MatrixType.MT_3D_4x4, 2, null, true, false, false);
        assertCol3D(MatrixType.MT_3D_4x4, 2, new double[3], true, false, false);
        assertCol3D(MatrixType.MT_3D_4x4, 2, new double[4], true, true, false);
        assertCol3D(MatrixType.MT_3D_4x4, 3, null, false, false, false);
        assertCol3D(MatrixType.MT_3D_4x4, 3, null, true, false, false);
        assertCol3D(MatrixType.MT_3D_4x4, 3, new double[3], true, false, false);
        assertCol3D(MatrixType.MT_3D_4x4, 3, new double[4], true, true, false);
        assertCol3D(MatrixType.MT_3D_4x4, -1, null, true, false, true);
        assertCol3D(MatrixType.MT_3D_4x4, 4, null, false, false, true);
    }

    @Test(expected=NullPointerException.class)
    public void testColumnNullType1() {
        t.column(null, 0);
    }

    @Test(expected=NullPointerException.class)
    public void testColumnNullType2() {
        t.column(null, 0, new double[] {});
    }

    @Test
    public void testSetOnTransformChanged() {
        Transform clone = t.clone();

        EventHandler<TransformChangedEvent> ontc =
                event -> {
                    eventCounter++;
                };

        assertSame(null, clone.getOnTransformChanged());
        clone.setOnTransformChanged(ontc);
        assertSame(ontc, clone.getOnTransformChanged());

        eventCounter = 0;
        if (TransformHelper.modify(clone, 42)) {
            if (t == rotateZeroAxis || t == noRotate) {
                // needs two changes to be further usable
                eventCounter--;
            }
            assertEquals(1, eventCounter);
            TransformHelper.modify(clone, 42);
            assertEquals(1, eventCounter);
            TransformHelper.modify(clone, 43);
            assertEquals(2, eventCounter);

            clone.setOnTransformChanged(null);
            assertNull(clone.getOnTransformChanged());
            TransformHelper.modify(clone, 44);
            assertEquals(2, eventCounter);

        } else {
            assertEquals(0, eventCounter);
        }
    }

    @Test
    public void testAddRemoveEventHandler() {
        Transform clone = t.clone();

        EventHandler<TransformChangedEvent> counting =
                event -> {
                    eventCounter++;
                };

        EventHandler<TransformChangedEvent> checking =
                event -> {
                    listenerCalled = true;
                };

        clone.addEventHandler(TransformChangedEvent.TRANSFORM_CHANGED, counting);
        clone.addEventHandler(TransformChangedEvent.TRANSFORM_CHANGED, checking);

        eventCounter = 0;
        listenerCalled = false;
        if (TransformHelper.modify(clone, 42)) {
            if (t == rotateZeroAxis || t == noRotate) {
                // needs two changes to be further usable
                eventCounter--;
            }
            assertEquals(1, eventCounter);
            assertTrue(listenerCalled);

            listenerCalled = false;
            TransformHelper.modify(clone, 42);
            assertEquals(1, eventCounter);
            assertFalse(listenerCalled);

            clone.removeEventHandler(TransformChangedEvent.TRANSFORM_CHANGED, checking);

            TransformHelper.modify(clone, 43);
            assertEquals(2, eventCounter);
            assertFalse(listenerCalled);
        } else {
            assertEquals(0, eventCounter);
        }
    }

    @Test
    public void testAddRemoveEventFilter() {
        Transform clone = t.clone();

        EventHandler<TransformChangedEvent> counting =
                event -> {
                    eventCounter++;
                    event.consume();
                };

        EventHandler<TransformChangedEvent> checking =
                event -> {
                    listenerCalled = true;
                };

        clone.addEventFilter(TransformChangedEvent.TRANSFORM_CHANGED, counting);
        clone.addEventHandler(TransformChangedEvent.TRANSFORM_CHANGED, checking);

        eventCounter = 0;
        listenerCalled = false;
        if (TransformHelper.modify(clone, 42)) {
            if (t == rotateZeroAxis || t == noRotate) {
                // needs two changes to be further usable
                eventCounter--;
            }
            assertEquals(1, eventCounter);
            assertFalse(listenerCalled); // consumed by filter

            listenerCalled = false;
            TransformHelper.modify(clone, 42);
            assertEquals(1, eventCounter);
            assertFalse(listenerCalled);

            clone.removeEventFilter(TransformChangedEvent.TRANSFORM_CHANGED, counting);

            TransformHelper.modify(clone, 43);
            assertEquals(1, eventCounter);
            assertTrue(listenerCalled);
        } else {
            assertEquals(0, eventCounter);
        }
    }
}
