package org.djutils.draw.line;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import org.djutils.draw.DrawRuntimeException;
import org.djutils.draw.point.Point2d;
import org.djutils.draw.point.Point3d;
import org.junit.Test;
/**
* Test the Bézier class.
*
* Copyright (c) 2013-2022 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved.
* BSD-style license. See OpenTrafficSim License.
*
* @version $Revision$, $LastChangedDate$, by $Author$, initial version Jan 2, 2017
* @author Alexander Verbraeck
* @author Peter Knoppers
* @author Wouter Schakel
*/
public class BezierTest
{
/**
* Test the various 2d methods in the Bezier class.
* @throws DrawRuntimeException when this happens uncaught this test has failed
* @throws DrawRuntimeException when this happens uncaught; this test has failed
*/
@Test
public final void bezierTest2d() throws DrawRuntimeException, DrawRuntimeException
{
Point2d from = new Point2d(10, 0);
Point2d control1 = new Point2d(20, 0);
Point2d control2 = new Point2d(00, 20);
Point2d to = new Point2d(0, 10);
for (int n : new int[] {2, 3, 4, 100})
{
PolyLine2d line = Bezier.cubic(n, from, control1, control2, to);
assertTrue("result has n points", line.size() == n);
assertTrue("result starts with from", line.get(0).equals(from));
assertTrue("result ends with to", line.get(line.size() - 1).equals(to));
for (int i = 1; i < line.size() - 1; i++)
{
Point2d p = line.get(i);
assertTrue("x of intermediate point has reasonable value", p.x > 0 && p.x < 15);
assertTrue("y of intermediate point has reasonable value", p.y > 0 && p.y < 15);
}
}
for (int n = -1; n <= 1; n++)
{
try
{
Bezier.cubic(n, from, control1, control2, to);
}
catch (DrawRuntimeException e)
{
// Ignore expected exception
}
}
for (int n : new int[] {2, 3, 4, 100})
{
for (double shape : new double[] {0.5, 1.0, 2.0})
{
for (boolean weighted : new boolean[] {false, true})
{
Ray2d start = new Ray2d(from.x, from.y, Math.PI / 2);
Ray2d end = new Ray2d(to.x, to.y, Math.PI);
PolyLine2d line = 1.0 == shape ? Bezier.cubic(n, start, end) : Bezier.cubic(n, start, end, shape, weighted);
for (int i = 1; i < line.size() - 1; i++)
{
Point2d p = line.get(i);
assertTrue("x of intermediate point has reasonable value", p.x > 0 && p.x < 15);
assertTrue("y of intermediate point has reasonable value", p.y > 0 && p.y < 15);
}
}
}
}
// Pity that the value 64 is private in the Bezier class.
assertEquals("Number of points is 64", 64,
Bezier.cubic(new Ray2d(from.x, from.y, Math.PI / 2), new Ray2d(to.x, to.y, -Math.PI / 2)).size());
assertEquals("Number of points is 64", 64, Bezier.bezier(from, control1, control2, to).size());
control1 = new Point2d(5, 0);
control2 = new Point2d(0, 5);
for (int n : new int[] {2, 3, 4, 100})
{
PolyLine2d line = Bezier.cubic(n, from, control1, control2, to);
for (int i = 1; i < line.size() - 1; i++)
{
Point2d p = line.get(i);
// System.out.println("Point " + i + " of " + n + " is " + p);
assertTrue("x of intermediate point has reasonable value", p.x > 0 && p.x < 10);
assertTrue("y of intermediate point has reasonable value", p.y > 0 && p.y < 10);
}
}
for (int n : new int[] {2, 3, 4, 100})
{
PolyLine2d line = Bezier.cubic(n, new Ray2d(from.x, from.y, Math.PI), new Ray2d(to.x, to.y, Math.PI / 2));
for (int i = 1; i < line.size() - 1; i++)
{
Point2d p = line.get(i);
assertTrue("x of intermediate point has reasonable value", p.x > 0 && p.x < 10);
assertTrue("y of intermediate point has reasonable value", p.y > 0 && p.y < 10);
}
}
Point2d start = new Point2d(1, 1);
Point2d c1 = new Point2d(11, 1);
// Point2d c3 = new Point2d(5, 1);
Point2d c2 = new Point2d(1, 11);
Point2d end = new Point2d(11, 11);
double autoDistance = start.distance(end) / 2;
Point2d c1Auto = new Point2d(start.x + autoDistance, start.y);
Point2d c2Auto = new Point2d(end.x - autoDistance, end.y);
// Should produce a right leaning S shape; something between a slash and an S
PolyLine2d reference = Bezier.bezier(256, start, c1, c2, end);
PolyLine2d referenceAuto = Bezier.bezier(256, start, c1Auto, c2Auto, end);
// System.out.print("ref " + reference.toPlot());
Ray2d startRay = new Ray2d(start, start.directionTo(c1));
Ray2d endRay = new Ray2d(end, c2.directionTo(end));
for (double epsilonPosition : new double[] {3, 1, 0.1, 0.05, 0.02})
{
// System.out.println("epsilonPosition " + epsilonPosition);
PolyLine2d line = Bezier.bezier(epsilonPosition, start, c1, c2, end);
// System.out.print("epsilonPosition " + epsilonPosition + " yields " + line.toPlot());
// for (int percent = 0; percent <= 100; percent++)
// {
// Ray2d ray = reference.getLocationFraction(percent / 100.0);
// double position = line.projectRay(ray);
// Point2d pointAtPosition = line.getLocation(position);
// double positionError = ray.distance(pointAtPosition);
// System.out.print(String.format(" %.3f", positionError));
// if (positionError >= epsilonPosition)
// {
// System.out.println();
// System.out.println("percent " + percent + ", on " + ray + " projected to " + pointAtPosition
// + " positionError " + positionError);
// }
// assertTrue("Actual error " + positionError + " exceeds epsilon " + epsilonPosition,
// positionError < epsilonPosition);
// }
// System.out.println();
compareBeziers("bezier with 2 explicit control points", reference, line, 100, epsilonPosition);
line = Bezier.cubic(epsilonPosition, start, c1, c2, end);
compareBeziers("cubic with 2 explicit control points", reference, line, 100, epsilonPosition);
line = Bezier.cubic(epsilonPosition, startRay, endRay);
compareBeziers("cubic with automatic control points", referenceAuto, line, 100, epsilonPosition);
}
try
{
Bezier.cubic(0.1, startRay, endRay, 0, true);
fail("Illegal shape value should have thrown a DrawRuntimeException");
}
catch (DrawRuntimeException dre)
{
// Ignore expected exception
}
try
{
Bezier.cubic(0.1, startRay, endRay, 0);
fail("Illegal shape value should have thrown a DrawRuntimeException");
}
catch (DrawRuntimeException dre)
{
// Ignore expected exception
}
try
{
Bezier.cubic(0.1, startRay, endRay, -1);
fail("Illegal shape value should have thrown a DrawRuntimeException");
}
catch (DrawRuntimeException dre)
{
// Ignore expected exception
}
try
{
Bezier.cubic(0.1, startRay, endRay, -1, true);
fail("Illegal shape value should have thrown a DrawRuntimeException");
}
catch (DrawRuntimeException dre)
{
// Ignore expected exception
}
try
{
Bezier.cubic(0.1, startRay, endRay, Double.NaN, true);
fail("Illegal shape value should have thrown a DrawRuntimeException");
}
catch (DrawRuntimeException dre)
{
// Ignore expected exception
}
try
{
Bezier.cubic(0.1, startRay, endRay, Double.NaN);
fail("Illegal shape value should have thrown a DrawRuntimeException");
}
catch (DrawRuntimeException dre)
{
// Ignore expected exception
}
try
{
Bezier.cubic(0.1, startRay, endRay, Double.POSITIVE_INFINITY);
fail("Illegal shape value should have thrown a DrawRuntimeException");
}
catch (DrawRuntimeException dre)
{
// Ignore expected exception
}
try
{
Bezier.cubic(0.1, startRay, endRay, Double.POSITIVE_INFINITY, true);
fail("Illegal shape value should have thrown a DrawRuntimeException");
}
catch (DrawRuntimeException dre)
{
// Ignore expected exception
}
try
{
Bezier.bezier(0.1, new Point2d[] {start});
fail("Too few points have thrown a DrawRuntimeException");
}
catch (DrawRuntimeException dre)
{
// Ignore expected exception
}
try
{
Bezier.bezier(0.1, new Point2d[] {});
fail("Too few points have thrown a DrawRuntimeException");
}
catch (DrawRuntimeException dre)
{
// Ignore expected exception
}
try
{
Bezier.bezier(0, start, c1, c2, end);
fail("illegal epsilon have thrown a DrawRuntimeException");
}
catch (DrawRuntimeException dre)
{
// Ignore expected exception
}
try
{
Bezier.bezier(-0.1, start, c1, c2, end);
fail("illegal epsilon have thrown a DrawRuntimeException");
}
catch (DrawRuntimeException dre)
{
// Ignore expected exception
}
try
{
Bezier.bezier(Double.NaN, start, c1, c2, end);
fail("illegal epsilon have thrown a DrawRuntimeException");
}
catch (DrawRuntimeException dre)
{
// Ignore expected exception
}
}
/**
* Compare Bézier curve approximations.
* @param description String; description of the test
* @param reference PolyLine2d; reference Bézier curve approximation
* @param candidate PolyLine2d; candidate Bézier curve approximation
* @param numberOfPoints int; number of point to compare the curves at, minus one; this method checks at 0% and at 100%
* @param epsilon double; upper limit of the distance between the two curves
* @throws DrawRuntimeException if that happens uncaught; a test has failed
*/
public void compareBeziers(final String description, final PolyLine2d reference, final PolyLine2d candidate,
final int numberOfPoints, final double epsilon) throws DrawRuntimeException
{
for (int step = 0; step <= numberOfPoints; step++)
{
double fraction = 1.0 * step / numberOfPoints;
Ray2d ray = reference.getLocationFraction(fraction);
double position = candidate.projectRay(ray);
Point2d pointAtPosition = candidate.getLocation(position);
double positionError = ray.distance(pointAtPosition);
if (positionError >= epsilon)
{
System.out.println("fraction " + fraction + ", on " + ray + " projected to " + pointAtPosition
+ " positionError " + positionError);
System.out.print("connection: " + new PolyLine2d(ray, pointAtPosition).toPlot());
System.out.print("reference: " + reference.toPlot());
System.out.print("candidate: " + candidate.toPlot());
}
assertTrue(description + " actual error is less than epsilon ", positionError < epsilon);
}
}
/**
* Test the various 3d methods in the Bezier class.
* @throws DrawRuntimeException when this happens uncaught this test has failed
*/
@Test
public final void bezierTest3d() throws DrawRuntimeException
{
Point3d from = new Point3d(10, 0, 0);
Point3d control1 = new Point3d(20, 0, 10);
Point3d control2 = new Point3d(0, 20, 20);
Point3d to = new Point3d(0, 10, 30);
for (int n : new int[] {2, 3, 4, 100})
{
PolyLine3d line = Bezier.cubic(n, from, control1, control2, to);
assertTrue("result has n points", line.size() == n);
assertTrue("result starts with from", line.get(0).equals(from));
assertTrue("result ends with to", line.get(line.size() - 1).equals(to));
for (int i = 1; i < line.size() - 1; i++)
{
Point3d p = line.get(i);
// System.out.println(p);
assertTrue("z of intermediate point has reasonable value", p.z > line.get(i - 1).z && p.z < line.get(i + 1).z);
assertTrue("x of intermediate point has reasonable value", p.x > 0 && p.x < 15);
assertTrue("y of intermediate point has reasonable value", p.y > 0 && p.y < 15);
}
}
for (int n = -1; n <= 1; n++)
{
try
{
Bezier.cubic(n, from, control1, control2, to);
}
catch (DrawRuntimeException e)
{
// Ignore expected exception
}
}
for (int n : new int[] {2, 3, 4, 100})
{
for (double shape : new double[] {0.5, 1.0, 2.0})
{
for (boolean weighted : new boolean[] {false, true})
{
Ray3d start = new Ray3d(from.x, from.y, from.z, Math.PI / 2, Math.PI / 3);
Ray3d end = new Ray3d(to.x, to.y, to.z, Math.PI, 0);
PolyLine3d line = 1.0 == shape ? Bezier.cubic(n, start, end) : Bezier.cubic(n, start, end, shape, weighted);
for (int i = 1; i < line.size() - 1; i++)
{
Point3d p = line.get(i);
// System.out.println(p);
assertTrue("x of intermediate point has reasonable value", p.x > -10 && p.x < 20);
assertTrue("y of intermediate point has reasonable value", p.y > -10 && p.y < 30);
assertTrue("z of intermediate point has reasonable value", p.z > -10 && p.z < 40);
}
}
}
}
// Pity that the value 64 is private in the Bezier class.
assertEquals("Number of points is 64", 64, Bezier.cubic(new Ray3d(from.x, from.y, from.z, Math.PI / 2, -Math.PI / 2),
new Ray3d(to.x, to.y, to.z, Math.PI, -Math.PI / 2)).size());
assertEquals("Number of points is 64", 64, Bezier.bezier(from, control1, control2, to).size());
control1 = new Point3d(5, 0, 10);
control2 = new Point3d(0, 5, 20);
for (int n : new int[] {2, 3, 4, 100})
{
PolyLine3d line = Bezier.cubic(n, from, control1, control2, to);
assertEquals("from x", from.x, line.getFirst().x, 0);
assertEquals("from y", from.y, line.getFirst().y, 0);
assertEquals("from z", from.z, line.getFirst().z, 0);
assertEquals("to x", to.x, line.getLast().x, 0);
assertEquals("to y", to.y, line.getLast().y, 0);
assertEquals("to z", to.z, line.getLast().z, 0);
for (int i = 0; i < line.size(); i++)
{
Point3d p = line.get(i);
// System.out.println(p);
assertTrue("x of intermediate point has reasonable value", p.x > -10 && p.x < 20);
assertTrue("y of intermediate point has reasonable value", p.y > -10 && p.y < 30);
assertTrue("z of intermediate point has reasonable value", p.z > -10 && p.z <= 30);
}
}
for (int n : new int[] {2, 3, 4, 100})
{
PolyLine3d line = Bezier.cubic(n, new Ray3d(from.x, from.y, from.z, Math.PI / 2, Math.PI / 2),
new Ray3d(to.x, to.y, to.z, 0, Math.PI / 2));
for (int i = 0; i < line.size(); i++)
{
Point3d p = line.get(i);
// System.out.println(p);
assertTrue("x of intermediate point has reasonable value", p.x > -10 && p.x < 20);
assertTrue("y of intermediate point has reasonable value", p.y > -10 && p.y < 30);
assertTrue("z of intermediate point has reasonable value", p.z > -10 && p.z <= 30);
}
}
}
/**
* Test the various exceptions of the 2d methods in the Bezier class.
*/
@Test
public void testExceptions2d()
{
Ray2d ray1 = new Ray2d(2, 3, 4);
Ray2d ray2 = new Ray2d(2, 3, 5);
Ray2d ray3 = new Ray2d(4, 5, 6);
Point2d cp1 = new Point2d(2.5, 13.5);
Point2d cp2 = new Point2d(3.5, 14.5);
try
{
Bezier.cubic(null, ray2);
fail("null should have thrown a NullPointerException");
}
catch (NullPointerException npe)
{
// Ignore expected exception
}
try
{
Bezier.cubic(ray1, null);
fail("null should have thrown a NullPointerException");
}
catch (NullPointerException npe)
{
// Ignore expected exception
}
try
{
Bezier.cubic(ray1, ray2);
fail("Coinciding start and end points should have thrown a DrawRuntimeException");
}
catch (DrawRuntimeException dre)
{
// Ignore expected exception
}
try
{
Bezier.cubic(Bezier.DEFAULT_BEZIER_SIZE, ray1, ray3, -1);
fail("Illegal shape value should have thrown a DrawRuntimeException");
}
catch (DrawRuntimeException dre)
{
// Ignore expected exception
}
try
{
Bezier.cubic(Bezier.DEFAULT_BEZIER_SIZE, ray1, ray3, Double.NaN);
fail("Illegal shape value should have thrown a DrawRuntimeException");
}
catch (DrawRuntimeException dre)
{
// Ignore expected exception
}
try
{
Bezier.cubic(Bezier.DEFAULT_BEZIER_SIZE, ray1, ray3, Double.POSITIVE_INFINITY);
fail("Illegal shape value should have thrown a DrawRuntimeException");
}
catch (DrawRuntimeException dre)
{
// Ignore expected exception
}
PolyLine2d result = Bezier.bezier(2, ray1, ray3); // Should succeed
assertEquals("size should be 2", 2, result.size());
assertEquals("start of result is at start", 0, ray1.distanceSquared(result.getFirst()), 0);
assertEquals("end of result is at start", 0, ray3.distanceSquared(result.getLast()), 0);
result = Bezier.bezier(ray1, ray3); // Should succeed
assertEquals("size should be default", Bezier.DEFAULT_BEZIER_SIZE, result.size());
assertEquals("start of result is at start", 0, ray1.distanceSquared(result.getFirst()), 0);
assertEquals("end of result is at start", 0, ray3.distanceSquared(result.getLast()), 0);
try
{
Bezier.bezier(1, ray1, ray3);
fail("size smaller than 2 should have thrown a DrawRuntimeException");
}
catch (DrawRuntimeException dre)
{
// Ignore expected exception
}
try
{
Bezier.bezier(ray1);
fail("cannot make a Bezier from only one point; should have thrown a DrawRuntimeException");
}
catch (DrawRuntimeException dre)
{
// Ignore expected exception
}
result = Bezier.cubic(2, ray1, cp1, cp2, ray3);
assertEquals("size should be 2", 2, result.size());
assertEquals("start of result is at start", 0, ray1.distanceSquared(result.getFirst()), 0);
assertEquals("end of result is at start", 0, ray3.distanceSquared(result.getLast()), 0);
result = Bezier.cubic(4, ray1, cp1, cp2, ray3);
assertEquals("size should be 4", 4, result.size());
assertEquals("start of result is at start", 0, ray1.distanceSquared(result.getFirst()), 0);
assertEquals("end of result is at start", 0, ray3.distanceSquared(result.getLast()), 0);
try
{
Bezier.cubic(1, ray1, cp1, cp2, ray3);
fail("Cannot construct a Bezier approximation that has only one point");
}
catch (DrawRuntimeException dre)
{
// Ignore expected exception
}
}
/**
* Test the various exceptions of the 3d methods in the Bezier class.
*/
@Test
public void testExceptions3d()
{
Ray3d ray1 = new Ray3d(2, 3, 4, 5, 6, 7);
Ray3d ray2 = new Ray3d(2, 3, 4, 7, 9, 11);
Ray3d ray3 = new Ray3d(4, 5, 6, 1, 2, 3);
Point3d cp1 = new Point3d(2.5, 13.5, 7);
Point3d cp2 = new Point3d(3.5, 14.5, 9);
try
{
Bezier.cubic(null, ray2);
fail("null should have thrown a NullPointerException");
}
catch (NullPointerException npe)
{
// Ignore expected exception
}
try
{
Bezier.cubic(ray1, null);
fail("null should have thrown a NullPointerException");
}
catch (NullPointerException npe)
{
// Ignore expected exception
}
try
{
Bezier.cubic(ray1, ray2);
fail("Coinciding start and end points should have thrown a DrawRuntimeException");
}
catch (DrawRuntimeException dre)
{
// Ignore expected exception
}
try
{
Bezier.cubic(Bezier.DEFAULT_BEZIER_SIZE, ray1, ray3, -1);
fail("Illegal shape value should have thrown a DrawRuntimeException");
}
catch (DrawRuntimeException dre)
{
// Ignore expected exception
}
try
{
Bezier.cubic(Bezier.DEFAULT_BEZIER_SIZE, ray1, ray3, Double.NaN);
fail("Illegal shape value should have thrown a DrawRuntimeException");
}
catch (DrawRuntimeException dre)
{
// Ignore expected exception
}
try
{
Bezier.cubic(Bezier.DEFAULT_BEZIER_SIZE, ray1, ray3, Double.POSITIVE_INFINITY);
fail("Illegal shape value should have thrown a DrawRuntimeException");
}
catch (DrawRuntimeException dre)
{
// Ignore expected exception
}
PolyLine3d result = Bezier.bezier(2, ray1, ray3); // Should succeed
assertEquals("size should be 2", 2, result.size());
assertEquals("start of result is at start", 0, ray1.distanceSquared(result.getFirst()), 0);
assertEquals("end of result is at start", 0, ray3.distanceSquared(result.getLast()), 0);
result = Bezier.bezier(ray1, ray3); // Should succeed
assertEquals("size should be default", Bezier.DEFAULT_BEZIER_SIZE, result.size());
assertEquals("start of result is at start", 0, ray1.distanceSquared(result.getFirst()), 0);
assertEquals("end of result is at start", 0, ray3.distanceSquared(result.getLast()), 0);
try
{
Bezier.bezier(1, ray1, ray3);
fail("size smaller than 2 should have thrown a DrawRuntimeException");
}
catch (DrawRuntimeException dre)
{
// Ignore expected exception
}
try
{
Bezier.bezier(ray1);
fail("cannot make a Bezier from only one point; should have thrown a DrawRuntimeException");
}
catch (DrawRuntimeException dre)
{
// Ignore expected exception
}
result = Bezier.cubic(2, ray1, cp1, cp2, ray3);
assertEquals("size should be 2", 2, result.size());
assertEquals("start of result is at start", 0, ray1.distanceSquared(result.getFirst()), 0);
assertEquals("end of result is at start", 0, ray3.distanceSquared(result.getLast()), 0);
result = Bezier.cubic(4, ray1, cp1, cp2, ray3);
assertEquals("size should be 4", 4, result.size());
assertEquals("start of result is at start", 0, ray1.distanceSquared(result.getFirst()), 0);
assertEquals("end of result is at start", 0, ray3.distanceSquared(result.getLast()), 0);
try
{
Bezier.cubic(1, ray1, cp1, cp2, ray3);
fail("Cannot construct a Bezier approximation that has only one point");
}
catch (DrawRuntimeException dre)
{
// Ignore expected exception
}
}
}