package org.djutils.draw;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import java.util.Arrays;
import org.djutils.draw.bounds.Bounds2d;
import org.djutils.draw.point.Point2d;
import org.junit.Test;
/**
* Transform2dTest.java.
*
* Copyright (c) 2020-2021 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved.
* BSD-style license. See DJUTILS License.
*
* @author Alexander Verbraeck
* @author Peter Knoppers
*/
public class Transform2dTest
{
/**
* Test the matrix / vector multiplication.
*/
@Test
public void testMatrixMultiplication()
{
double[] mA = new double[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
double[] mB = new double[] { 2, 1, 0, 2, 4, 3, 3, 1, 2 };
double[] mAmB = Transform2d.mulMatMat(mA, mB);
double[] expected = new double[] { 15, 12, 12, 36, 30, 27, 57, 48, 42 };
for (int i = 0; i < 9; i++)
{
if (mAmB[i] != expected[i])
{
fail(String.format("difference MA x MB at %d: expected %f, was: %f", i, expected[i], mAmB[i]));
}
}
double[] m = new double[] { 1, 4, 2, 5, 3, 1, 4, 2, 5 };
double[] v = new double[] { 2, 5, 1 };
double[] mv = Transform2d.mulMatVec(m, v);
double[] ev = new double[] { 24, 26, 23 };
for (int i = 0; i < 3; i++)
{
if (mv[i] != ev[i])
{
fail(String.format("difference M x V at %d: expected %f, was: %f", i, ev[i], mv[i]));
}
}
v = new double[] { 1, 2 };
mv = Transform2d.mulMatVec2(m, v);
ev = new double[] { 11, 12 };
for (int i = 0; i < 2; i++)
{
if (mv[i] != ev[i])
{
fail(String.format("difference M x V3 at %d: expected %f, was: %f", i, ev[i], mv[i]));
}
}
}
/**
* Test that the constructor creates an Identity matrix.
*/
@Test
public void testConstructor()
{
// TODO: decide whether the internal (flattened) matrix should be visible at all, or add a getter
Transform2d t = new Transform2d();
assertEquals("matrix contians 9 values", 9, t.getMat().length);
for (int row = 0; row < 3; row++)
{
for (int col = 0; col < 3; col++)
{
int e = row == col ? 1 : 0;
assertEquals("Value in identity matrix matches", e, t.getMat()[3 * row + col], 0);
}
}
}
/**
* Test the translate, scale, rotate, shear and reflect methods.
*/
@Test
public void testTranslateScaleRotateShearAndReflect()
{
Transform2d t;
// Test time grows (explodes) with the 4th power of the length of values.
double[] values = new double[] { -100000, -100, -10, -3, -1, -0.3, -0.1, 0, 0.1, 0.3, 1, 3, 10, 100, 100000 };
for (double dx : values)
{
for (double dy : values)
{
// Translate defined with a double[]
t = new Transform2d();
t.translate(dx, dy);
for (double px : values)
{
for (double py : values)
{
Point2d p = t.transform(new Point2d(px, py));
assertEquals("translated x matches", px + dx, p.x, 0.001);
assertEquals("translated y matches", py + dy, p.y, 0.001);
double[] result = t.transform(new double[] { px, py });
assertEquals("translated x matches", px + dx, result[0], 0.001);
assertEquals("translated y matches", py + dy, result[1], 0.001);
}
}
// Translate defined with a Point
t = new Transform2d();
t.translate(new Point2d(dx, dy));
for (double px : values)
{
for (double py : values)
{
Point2d p = t.transform(new Point2d(px, py));
assertEquals("transformed x matches", px + dx, p.x, 0.001);
assertEquals("transformed y matches", py + dy, p.y, 0.001);
double[] result = t.transform(new double[] { px, py });
assertEquals("transformed x matches", px + dx, result[0], 0.001);
assertEquals("transformed y matches", py + dy, result[1], 0.001);
}
}
// Scale
t = new Transform2d();
t.scale(dx, dy);
for (double px : values)
{
for (double py : values)
{
Point2d p = t.transform(new Point2d(px, py));
assertEquals("scaled x matches", px * dx, p.x, 0.001);
assertEquals("scaled y matches", py * dy, p.y, 0.001);
double[] result = t.transform(new double[] { px, py });
assertEquals("scaled x matches", px * dx, result[0], 0.001);
assertEquals("scaled y matches", py * dy, result[1], 0.001);
}
}
// Shear
t = new Transform2d();
t.shear(dx, dy);
for (double px : values)
{
for (double py : values)
{
Point2d p = t.transform(new Point2d(px, py));
assertEquals("sheared x matches", px + py * dx, p.x, 0.001);
assertEquals("sheared y matches", py + px * dy, p.y, 0.001);
double[] result = t.transform(new double[] { px, py });
assertEquals("sheared x matches", px + py * dx, result[0], 0.001);
assertEquals("sheared y matches", py + px * dy, result[1], 0.001);
}
}
}
// Rotate (using dx as angle)
t = new Transform2d();
t.rotation(dx);
double sine = Math.sin(dx);
double cosine = Math.cos(dx);
for (double px : values)
{
for (double py : values)
{
Point2d p = t.transform(new Point2d(px, py));
assertEquals("rotated x matches", px * cosine - py * sine, p.x, 0.001);
assertEquals("rotated y matches", py * cosine + px * sine, p.y, 0.001);
double[] result = t.transform(new double[] { px, py });
assertEquals("rotated x matches", px * cosine - py * sine, result[0], 0.001);
assertEquals("rotated y matches", py * cosine + px * sine, result[1], 0.001);
}
}
}
// ReflectX
t = new Transform2d();
t.reflectX();
for (double px : values)
{
for (double py : values)
{
Point2d p = t.transform(new Point2d(px, py));
assertEquals("x-reflected x matches", -px, p.x, 0.001);
assertEquals("x-reflected y matches", py, p.y, 0.001);
double[] result = t.transform(new double[] { px, py });
assertEquals("x-reflected x matches", -px, result[0], 0.001);
assertEquals("x-reflected y matches", py, result[1], 0.001);
}
}
// ReflectY
t = new Transform2d();
t.reflectY();
for (double px : values)
{
for (double py : values)
{
Point2d p = t.transform(new Point2d(px, py));
assertEquals("y-reflected x matches", px, p.x, 0.001);
assertEquals("y-reflected y matches", -py, p.y, 0.001);
double[] result = t.transform(new double[] { px, py });
assertEquals("y-reflected x matches", px, result[0], 0.001);
assertEquals("y-reflected y matches", -py, result[1], 0.001);
}
}
}
/**
* Test the transform method.
*/
@Test
public void transformTest()
{
Transform2d reflectionX = new Transform2d().reflectX();
Transform2d reflectionY = new Transform2d().reflectY();
// Test time explodes with the 6th power of the length of this array
double[] values = new double[] { -100, -0.1, 0, 0.01, 1, 100 };
for (double translateX : values)
{
for (double translateY : values)
{
Transform2d translation = new Transform2d().translate(translateX, translateY);
for (double scaleX : values)
{
for (double scaleY : values)
{
Transform2d scaling = new Transform2d().scale(scaleX, scaleY);
for (double angle : new double[] { -2, 0, 0.5 })
{
Transform2d rotation = new Transform2d().rotation(angle);
for (double shearX : values)
{
for (double shearY : values)
{
Transform2d t = new Transform2d().translate(translateX, translateY).scale(scaleX, scaleY)
.rotation(angle).shear(shearX, shearY);
Transform2d tReflectX = new Transform2d().reflectX().translate(translateX, translateY)
.scale(scaleX, scaleY).rotation(angle).shear(shearX, shearY);
Transform2d tReflectY = new Transform2d().reflectY().translate(translateX, translateY)
.scale(scaleX, scaleY).rotation(angle).shear(shearX, shearY);
Transform2d shearing = new Transform2d().shear(shearX, shearY);
for (double px : values)
{
for (double py : values)
{
Point2d p = new Point2d(px, py);
Point2d tp = t.transform(p);
Point2d chainP = translation
.transform(scaling.transform(rotation.transform(shearing.transform(p))));
assertEquals("X", chainP.x, tp.x, 0.0000001);
assertEquals("Y", chainP.y, tp.y, 0.0000001);
tp = tReflectX.transform(p);
Point2d chainPReflectX = reflectionX.transform(chainP);
assertEquals("RX X", chainPReflectX.x, tp.x, 0.0000001);
assertEquals("RX Y", chainPReflectX.y, tp.y, 0.0000001);
tp = tReflectY.transform(p);
Point2d chainPReflectY = reflectionY.transform(chainP);
assertEquals("RY X", chainPReflectY.x, tp.x, 0.0000001);
assertEquals("RY Y", chainPReflectY.y, tp.y, 0.0000001);
}
}
}
}
}
}
}
}
}
}
/**
* Test transformation of a bounding rectangle.
*/
@Test
public void transformBounds2dTest()
{
double[] values = new double[] { -100, 0.1, 0, 0.1, 100 };
double[] sizes = new double[] { 0, 10, 100 };
Transform2d t = new Transform2d().rotation(0.4).reflectX().scale(0.5, 1.5).shear(2, 3).translate(123, 456);
// System.out.println(t);
for (double x : values)
{
for (double y : values)
{
for (double xSize : sizes)
{
for (double ySize : sizes)
{
Bounds2d bb = new Bounds2d(x, x + xSize, y, y + ySize);
Point2d[] points = new Point2d[] { new Point2d(x, y), new Point2d(x + xSize, y),
new Point2d(x, y + ySize), new Point2d(x + xSize, y + ySize) };
Point2d[] transformedPoints = new Point2d[4];
for (int i = 0; i < points.length; i++)
{
transformedPoints[i] = t.transform(points[i]);
}
Bounds2d expected = new Bounds2d(Arrays.stream(transformedPoints).iterator());
Bounds2d got = t.transform(bb);
assertEquals("bb minX", expected.getMinX(), got.getMinX(), 0.0001);
assertEquals("bb maxX", expected.getMaxX(), got.getMaxX(), 0.0001);
assertEquals("bb minY", expected.getMinY(), got.getMinY(), 0.0001);
assertEquals("bb maxY", expected.getMaxY(), got.getMaxY(), 0.0001);
}
}
}
}
}
/**
* Reproducible test of multiple transformations on a bounding rectangle.
*/
@Test
public void testBoundingRectangle2d()
{
Bounds2d bounds = new Bounds2d(-4, 4, -4, 4);
// identical transformation
Transform2d transform = new Transform2d();
Bounds2d b = transform.transform(bounds);
testBounds2d(b, -4, 4, -4, 4);
// translate x, y
transform = new Transform2d();
transform.translate(20, 10);
b = transform.transform(bounds);
testBounds2d(b, 20 - 4, 20 + 4, 10 - 4, 10 + 4);
// rotate 90 degrees (should be same)
transform = new Transform2d();
transform.rotation(Math.toRadians(90.0));
b = transform.transform(bounds);
testBounds2d(b, -4, 4, -4, 4);
// rotate 45 degrees in the XY-plane
transform = new Transform2d();
transform.rotation(Math.toRadians(45.0));
double d = 4.0 * Math.sqrt(2.0);
b = transform.transform(bounds);
testBounds2d(b, -d, d, -d, d);
// rotate 45 degrees in the XY-plane and then translate to (10, 20)
// note that to do FIRST rotation and THEN translation, the steps have to be built in the OPPOSITE order
// since matrix multiplication operates from RIGHT to LEFT.
transform = new Transform2d();
transform.translate(10, 20);
transform.rotation(Math.toRadians(45.0));
b = transform.transform(bounds);
testBounds2d(b, 10 - d, 10 + d, 20 - d, 20 + d);
}
/**
* Check bounds values.
* @param b Bounds2d; the box to test
* @param minX double; expected value
* @param maxX double; expected value
* @param minY double; expected value
* @param maxY double; expected value
*/
private void testBounds2d(final Bounds2d b, final double minX, final double maxX, final double minY, final double maxY)
{
assertEquals(minX, b.getMinX(), 0.001);
assertEquals(maxX, b.getMaxX(), 0.001);
assertEquals(minY, b.getMinY(), 0.001);
assertEquals(maxY, b.getMaxY(), 0.001);
}
/**
* Check that toString returns something descriptive.
*/
@Test
public void toStringTest()
{
assertTrue("toString returns something descriptive", new Transform2d().toString().startsWith("Transform2d "));
}
}