1 package org.djutils.draw.line;
2
3 import static org.junit.Assert.assertEquals;
4 import static org.junit.Assert.assertTrue;
5 import static org.junit.Assert.fail;
6
7 import org.djutils.draw.DrawRuntimeException;
8 import org.djutils.draw.point.Point2d;
9 import org.djutils.draw.point.Point3d;
10 import org.junit.Test;
11
12
13
14
15
16
17
18
19
20
21
22
23 public class BezierTest
24 {
25
26
27
28
29
30
31 @Test
32 public final void bezierTest2d() throws DrawRuntimeException, DrawRuntimeException
33 {
34 Point2d from = new Point2d(10, 0);
35 Point2d control1 = new Point2d(20, 0);
36 Point2d control2 = new Point2d(00, 20);
37 Point2d to = new Point2d(0, 10);
38 for (int n : new int[] {2, 3, 4, 100})
39 {
40 PolyLine2d line = Bezier.cubic(n, from, control1, control2, to);
41 assertTrue("result has n points", line.size() == n);
42 assertTrue("result starts with from", line.get(0).equals(from));
43 assertTrue("result ends with to", line.get(line.size() - 1).equals(to));
44 for (int i = 1; i < line.size() - 1; i++)
45 {
46 Point2d p = line.get(i);
47 assertTrue("x of intermediate point has reasonable value", p.x > 0 && p.x < 15);
48 assertTrue("y of intermediate point has reasonable value", p.y > 0 && p.y < 15);
49 }
50 }
51 for (int n = -1; n <= 1; n++)
52 {
53 try
54 {
55 Bezier.cubic(n, from, control1, control2, to);
56 }
57 catch (DrawRuntimeException e)
58 {
59
60 }
61 }
62 for (int n : new int[] {2, 3, 4, 100})
63 {
64 for (double shape : new double[] {0.5, 1.0, 2.0})
65 {
66 for (boolean weighted : new boolean[] {false, true})
67 {
68 Ray2d start = new Ray2d(from.x, from.y, Math.PI / 2);
69 Ray2d end = new Ray2d(to.x, to.y, Math.PI);
70 PolyLine2d line = 1.0 == shape ? Bezier.cubic(n, start, end) : Bezier.cubic(n, start, end, shape, weighted);
71 for (int i = 1; i < line.size() - 1; i++)
72 {
73 Point2d p = line.get(i);
74 assertTrue("x of intermediate point has reasonable value", p.x > 0 && p.x < 15);
75 assertTrue("y of intermediate point has reasonable value", p.y > 0 && p.y < 15);
76 }
77 }
78 }
79 }
80
81 assertEquals("Number of points is 64", 64,
82 Bezier.cubic(new Ray2d(from.x, from.y, Math.PI / 2), new Ray2d(to.x, to.y, -Math.PI / 2)).size());
83 assertEquals("Number of points is 64", 64, Bezier.bezier(from, control1, control2, to).size());
84 control1 = new Point2d(5, 0);
85 control2 = new Point2d(0, 5);
86 for (int n : new int[] {2, 3, 4, 100})
87 {
88 PolyLine2d line = Bezier.cubic(n, from, control1, control2, to);
89 for (int i = 1; i < line.size() - 1; i++)
90 {
91 Point2d p = line.get(i);
92
93 assertTrue("x of intermediate point has reasonable value", p.x > 0 && p.x < 10);
94 assertTrue("y of intermediate point has reasonable value", p.y > 0 && p.y < 10);
95 }
96 }
97 for (int n : new int[] {2, 3, 4, 100})
98 {
99 PolyLine2d line = Bezier.cubic(n, new Ray2d(from.x, from.y, Math.PI), new Ray2d(to.x, to.y, Math.PI / 2));
100 for (int i = 1; i < line.size() - 1; i++)
101 {
102 Point2d p = line.get(i);
103 assertTrue("x of intermediate point has reasonable value", p.x > 0 && p.x < 10);
104 assertTrue("y of intermediate point has reasonable value", p.y > 0 && p.y < 10);
105 }
106 }
107
108 Point2d start = new Point2d(1, 1);
109 Point2d c1 = new Point2d(11, 1);
110
111 Point2d c2 = new Point2d(1, 11);
112 Point2d end = new Point2d(11, 11);
113 double autoDistance = start.distance(end) / 2;
114 Point2d c1Auto = new Point2d(start.x + autoDistance, start.y);
115 Point2d c2Auto = new Point2d(end.x - autoDistance, end.y);
116
117 PolyLine2d reference = Bezier.bezier(256, start, c1, c2, end);
118 PolyLine2d referenceAuto = Bezier.bezier(256, start, c1Auto, c2Auto, end);
119
120 Ray2d startRay = new Ray2d(start, start.directionTo(c1));
121 Ray2d endRay = new Ray2d(end, c2.directionTo(end));
122 for (double epsilonPosition : new double[] {3, 1, 0.1, 0.05, 0.02})
123 {
124
125 PolyLine2d line = Bezier.bezier(epsilonPosition, start, c1, c2, end);
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144 compareBeziers("bezier with 2 explicit control points", reference, line, 100, epsilonPosition);
145 line = Bezier.cubic(epsilonPosition, start, c1, c2, end);
146 compareBeziers("cubic with 2 explicit control points", reference, line, 100, epsilonPosition);
147 line = Bezier.cubic(epsilonPosition, startRay, endRay);
148 compareBeziers("cubic with automatic control points", referenceAuto, line, 100, epsilonPosition);
149 }
150
151 try
152 {
153 Bezier.cubic(0.1, startRay, endRay, 0, true);
154 fail("Illegal shape value should have thrown a DrawRuntimeException");
155 }
156 catch (DrawRuntimeException dre)
157 {
158
159 }
160
161 try
162 {
163 Bezier.cubic(0.1, startRay, endRay, 0);
164 fail("Illegal shape value should have thrown a DrawRuntimeException");
165 }
166 catch (DrawRuntimeException dre)
167 {
168
169 }
170
171 try
172 {
173 Bezier.cubic(0.1, startRay, endRay, -1);
174 fail("Illegal shape value should have thrown a DrawRuntimeException");
175 }
176 catch (DrawRuntimeException dre)
177 {
178
179 }
180
181 try
182 {
183 Bezier.cubic(0.1, startRay, endRay, -1, true);
184 fail("Illegal shape value should have thrown a DrawRuntimeException");
185 }
186 catch (DrawRuntimeException dre)
187 {
188
189 }
190
191 try
192 {
193 Bezier.cubic(0.1, startRay, endRay, Double.NaN, true);
194 fail("Illegal shape value should have thrown a DrawRuntimeException");
195 }
196 catch (DrawRuntimeException dre)
197 {
198
199 }
200
201 try
202 {
203 Bezier.cubic(0.1, startRay, endRay, Double.NaN);
204 fail("Illegal shape value should have thrown a DrawRuntimeException");
205 }
206 catch (DrawRuntimeException dre)
207 {
208
209 }
210
211 try
212 {
213 Bezier.cubic(0.1, startRay, endRay, Double.POSITIVE_INFINITY);
214 fail("Illegal shape value should have thrown a DrawRuntimeException");
215 }
216 catch (DrawRuntimeException dre)
217 {
218
219 }
220
221 try
222 {
223 Bezier.cubic(0.1, startRay, endRay, Double.POSITIVE_INFINITY, true);
224 fail("Illegal shape value should have thrown a DrawRuntimeException");
225 }
226 catch (DrawRuntimeException dre)
227 {
228
229 }
230
231 try
232 {
233 Bezier.bezier(0.1, new Point2d[] {start});
234 fail("Too few points have thrown a DrawRuntimeException");
235 }
236 catch (DrawRuntimeException dre)
237 {
238
239 }
240
241 try
242 {
243 Bezier.bezier(0.1, new Point2d[] {});
244 fail("Too few points have thrown a DrawRuntimeException");
245 }
246 catch (DrawRuntimeException dre)
247 {
248
249 }
250
251 try
252 {
253 Bezier.bezier(0, start, c1, c2, end);
254 fail("illegal epsilon have thrown a DrawRuntimeException");
255 }
256 catch (DrawRuntimeException dre)
257 {
258
259 }
260
261 try
262 {
263 Bezier.bezier(-0.1, start, c1, c2, end);
264 fail("illegal epsilon have thrown a DrawRuntimeException");
265 }
266 catch (DrawRuntimeException dre)
267 {
268
269 }
270
271 try
272 {
273 Bezier.bezier(Double.NaN, start, c1, c2, end);
274 fail("illegal epsilon have thrown a DrawRuntimeException");
275 }
276 catch (DrawRuntimeException dre)
277 {
278
279 }
280
281 }
282
283
284
285
286
287
288
289
290
291
292 public void compareBeziers(final String description, final PolyLine2d reference, final PolyLine2d candidate,
293 final int numberOfPoints, final double epsilon) throws DrawRuntimeException
294 {
295 for (int step = 0; step <= numberOfPoints; step++)
296 {
297 double fraction = 1.0 * step / numberOfPoints;
298 Ray2d ray = reference.getLocationFraction(fraction);
299 double position = candidate.projectRay(ray);
300 Point2d pointAtPosition = candidate.getLocation(position);
301 double positionError = ray.distance(pointAtPosition);
302 if (positionError >= epsilon)
303 {
304 System.out.println("fraction " + fraction + ", on " + ray + " projected to " + pointAtPosition
305 + " positionError " + positionError);
306 System.out.print("connection: " + new PolyLine2d(ray, pointAtPosition).toPlot());
307 System.out.print("reference: " + reference.toPlot());
308 System.out.print("candidate: " + candidate.toPlot());
309 }
310 assertTrue(description + " actual error is less than epsilon ", positionError < epsilon);
311 }
312 }
313
314
315
316
317
318 @Test
319 public final void bezierTest3d() throws DrawRuntimeException
320 {
321 Point3d from = new Point3d(10, 0, 0);
322 Point3d control1 = new Point3d(20, 0, 10);
323 Point3d control2 = new Point3d(0, 20, 20);
324 Point3d to = new Point3d(0, 10, 30);
325 for (int n : new int[] {2, 3, 4, 100})
326 {
327 PolyLine3d line = Bezier.cubic(n, from, control1, control2, to);
328 assertTrue("result has n points", line.size() == n);
329 assertTrue("result starts with from", line.get(0).equals(from));
330 assertTrue("result ends with to", line.get(line.size() - 1).equals(to));
331 for (int i = 1; i < line.size() - 1; i++)
332 {
333 Point3d p = line.get(i);
334
335 assertTrue("z of intermediate point has reasonable value", p.z > line.get(i - 1).z && p.z < line.get(i + 1).z);
336 assertTrue("x of intermediate point has reasonable value", p.x > 0 && p.x < 15);
337 assertTrue("y of intermediate point has reasonable value", p.y > 0 && p.y < 15);
338 }
339 }
340 for (int n = -1; n <= 1; n++)
341 {
342 try
343 {
344 Bezier.cubic(n, from, control1, control2, to);
345 }
346 catch (DrawRuntimeException e)
347 {
348
349 }
350 }
351 for (int n : new int[] {2, 3, 4, 100})
352 {
353 for (double shape : new double[] {0.5, 1.0, 2.0})
354 {
355 for (boolean weighted : new boolean[] {false, true})
356 {
357 Ray3d start = new Ray3d(from.x, from.y, from.z, Math.PI / 2, Math.PI / 3);
358 Ray3d end = new Ray3d(to.x, to.y, to.z, Math.PI, 0);
359 PolyLine3d line = 1.0 == shape ? Bezier.cubic(n, start, end) : Bezier.cubic(n, start, end, shape, weighted);
360 for (int i = 1; i < line.size() - 1; i++)
361 {
362 Point3d p = line.get(i);
363
364 assertTrue("x of intermediate point has reasonable value", p.x > -10 && p.x < 20);
365 assertTrue("y of intermediate point has reasonable value", p.y > -10 && p.y < 30);
366 assertTrue("z of intermediate point has reasonable value", p.z > -10 && p.z < 40);
367 }
368 }
369 }
370 }
371
372 assertEquals("Number of points is 64", 64, Bezier.cubic(new Ray3d(from.x, from.y, from.z, Math.PI / 2, -Math.PI / 2),
373 new Ray3d(to.x, to.y, to.z, Math.PI, -Math.PI / 2)).size());
374 assertEquals("Number of points is 64", 64, Bezier.bezier(from, control1, control2, to).size());
375 control1 = new Point3d(5, 0, 10);
376 control2 = new Point3d(0, 5, 20);
377 for (int n : new int[] {2, 3, 4, 100})
378 {
379 PolyLine3d line = Bezier.cubic(n, from, control1, control2, to);
380 assertEquals("from x", from.x, line.getFirst().x, 0);
381 assertEquals("from y", from.y, line.getFirst().y, 0);
382 assertEquals("from z", from.z, line.getFirst().z, 0);
383 assertEquals("to x", to.x, line.getLast().x, 0);
384 assertEquals("to y", to.y, line.getLast().y, 0);
385 assertEquals("to z", to.z, line.getLast().z, 0);
386 for (int i = 0; i < line.size(); i++)
387 {
388 Point3d p = line.get(i);
389
390 assertTrue("x of intermediate point has reasonable value", p.x > -10 && p.x < 20);
391 assertTrue("y of intermediate point has reasonable value", p.y > -10 && p.y < 30);
392 assertTrue("z of intermediate point has reasonable value", p.z > -10 && p.z <= 30);
393 }
394 }
395 for (int n : new int[] {2, 3, 4, 100})
396 {
397 PolyLine3d line = Bezier.cubic(n, new Ray3d(from.x, from.y, from.z, Math.PI / 2, Math.PI / 2),
398 new Ray3d(to.x, to.y, to.z, 0, Math.PI / 2));
399 for (int i = 0; i < line.size(); i++)
400 {
401 Point3d p = line.get(i);
402
403 assertTrue("x of intermediate point has reasonable value", p.x > -10 && p.x < 20);
404 assertTrue("y of intermediate point has reasonable value", p.y > -10 && p.y < 30);
405 assertTrue("z of intermediate point has reasonable value", p.z > -10 && p.z <= 30);
406 }
407 }
408 }
409
410
411
412
413 @Test
414 public void testExceptions2d()
415 {
416 Ray2d ray1 = new Ray2d(2, 3, 4);
417 Ray2d ray2 = new Ray2d(2, 3, 5);
418 Ray2d ray3 = new Ray2d(4, 5, 6);
419 Point2d cp1 = new Point2d(2.5, 13.5);
420 Point2d cp2 = new Point2d(3.5, 14.5);
421 try
422 {
423 Bezier.cubic(null, ray2);
424 fail("null should have thrown a NullPointerException");
425 }
426 catch (NullPointerException npe)
427 {
428
429 }
430
431 try
432 {
433 Bezier.cubic(ray1, null);
434 fail("null should have thrown a NullPointerException");
435 }
436 catch (NullPointerException npe)
437 {
438
439 }
440
441 try
442 {
443 Bezier.cubic(ray1, ray2);
444 fail("Coinciding start and end points should have thrown a DrawRuntimeException");
445 }
446 catch (DrawRuntimeException dre)
447 {
448
449 }
450
451 try
452 {
453 Bezier.cubic(Bezier.DEFAULT_BEZIER_SIZE, ray1, ray3, -1);
454 fail("Illegal shape value should have thrown a DrawRuntimeException");
455 }
456 catch (DrawRuntimeException dre)
457 {
458
459 }
460
461 try
462 {
463 Bezier.cubic(Bezier.DEFAULT_BEZIER_SIZE, ray1, ray3, Double.NaN);
464 fail("Illegal shape value should have thrown a DrawRuntimeException");
465 }
466 catch (DrawRuntimeException dre)
467 {
468
469 }
470
471 try
472 {
473 Bezier.cubic(Bezier.DEFAULT_BEZIER_SIZE, ray1, ray3, Double.POSITIVE_INFINITY);
474 fail("Illegal shape value should have thrown a DrawRuntimeException");
475 }
476 catch (DrawRuntimeException dre)
477 {
478
479 }
480
481 PolyLine2d result = Bezier.bezier(2, ray1, ray3);
482 assertEquals("size should be 2", 2, result.size());
483 assertEquals("start of result is at start", 0, ray1.distanceSquared(result.getFirst()), 0);
484 assertEquals("end of result is at start", 0, ray3.distanceSquared(result.getLast()), 0);
485 result = Bezier.bezier(ray1, ray3);
486 assertEquals("size should be default", Bezier.DEFAULT_BEZIER_SIZE, result.size());
487 assertEquals("start of result is at start", 0, ray1.distanceSquared(result.getFirst()), 0);
488 assertEquals("end of result is at start", 0, ray3.distanceSquared(result.getLast()), 0);
489 try
490 {
491 Bezier.bezier(1, ray1, ray3);
492 fail("size smaller than 2 should have thrown a DrawRuntimeException");
493 }
494 catch (DrawRuntimeException dre)
495 {
496
497 }
498
499 try
500 {
501 Bezier.bezier(ray1);
502 fail("cannot make a Bezier from only one point; should have thrown a DrawRuntimeException");
503 }
504 catch (DrawRuntimeException dre)
505 {
506
507 }
508
509 result = Bezier.cubic(2, ray1, cp1, cp2, ray3);
510 assertEquals("size should be 2", 2, result.size());
511 assertEquals("start of result is at start", 0, ray1.distanceSquared(result.getFirst()), 0);
512 assertEquals("end of result is at start", 0, ray3.distanceSquared(result.getLast()), 0);
513 result = Bezier.cubic(4, ray1, cp1, cp2, ray3);
514 assertEquals("size should be 4", 4, result.size());
515 assertEquals("start of result is at start", 0, ray1.distanceSquared(result.getFirst()), 0);
516 assertEquals("end of result is at start", 0, ray3.distanceSquared(result.getLast()), 0);
517
518 try
519 {
520 Bezier.cubic(1, ray1, cp1, cp2, ray3);
521 fail("Cannot construct a Bezier approximation that has only one point");
522 }
523 catch (DrawRuntimeException dre)
524 {
525
526 }
527
528 }
529
530
531
532
533 @Test
534 public void testExceptions3d()
535 {
536 Ray3d ray1 = new Ray3d(2, 3, 4, 5, 6, 7);
537 Ray3d ray2 = new Ray3d(2, 3, 4, 7, 9, 11);
538 Ray3d ray3 = new Ray3d(4, 5, 6, 1, 2, 3);
539 Point3d cp1 = new Point3d(2.5, 13.5, 7);
540 Point3d cp2 = new Point3d(3.5, 14.5, 9);
541 try
542 {
543 Bezier.cubic(null, ray2);
544 fail("null should have thrown a NullPointerException");
545 }
546 catch (NullPointerException npe)
547 {
548
549 }
550
551 try
552 {
553 Bezier.cubic(ray1, null);
554 fail("null should have thrown a NullPointerException");
555 }
556 catch (NullPointerException npe)
557 {
558
559 }
560
561 try
562 {
563 Bezier.cubic(ray1, ray2);
564 fail("Coinciding start and end points should have thrown a DrawRuntimeException");
565 }
566 catch (DrawRuntimeException dre)
567 {
568
569 }
570
571 try
572 {
573 Bezier.cubic(Bezier.DEFAULT_BEZIER_SIZE, ray1, ray3, -1);
574 fail("Illegal shape value should have thrown a DrawRuntimeException");
575 }
576 catch (DrawRuntimeException dre)
577 {
578
579 }
580
581 try
582 {
583 Bezier.cubic(Bezier.DEFAULT_BEZIER_SIZE, ray1, ray3, Double.NaN);
584 fail("Illegal shape value should have thrown a DrawRuntimeException");
585 }
586 catch (DrawRuntimeException dre)
587 {
588
589 }
590
591 try
592 {
593 Bezier.cubic(Bezier.DEFAULT_BEZIER_SIZE, ray1, ray3, Double.POSITIVE_INFINITY);
594 fail("Illegal shape value should have thrown a DrawRuntimeException");
595 }
596 catch (DrawRuntimeException dre)
597 {
598
599 }
600
601 PolyLine3d result = Bezier.bezier(2, ray1, ray3);
602 assertEquals("size should be 2", 2, result.size());
603 assertEquals("start of result is at start", 0, ray1.distanceSquared(result.getFirst()), 0);
604 assertEquals("end of result is at start", 0, ray3.distanceSquared(result.getLast()), 0);
605 result = Bezier.bezier(ray1, ray3);
606 assertEquals("size should be default", Bezier.DEFAULT_BEZIER_SIZE, result.size());
607 assertEquals("start of result is at start", 0, ray1.distanceSquared(result.getFirst()), 0);
608 assertEquals("end of result is at start", 0, ray3.distanceSquared(result.getLast()), 0);
609 try
610 {
611 Bezier.bezier(1, ray1, ray3);
612 fail("size smaller than 2 should have thrown a DrawRuntimeException");
613 }
614 catch (DrawRuntimeException dre)
615 {
616
617 }
618
619 try
620 {
621 Bezier.bezier(ray1);
622 fail("cannot make a Bezier from only one point; should have thrown a DrawRuntimeException");
623 }
624 catch (DrawRuntimeException dre)
625 {
626
627 }
628
629 result = Bezier.cubic(2, ray1, cp1, cp2, ray3);
630 assertEquals("size should be 2", 2, result.size());
631 assertEquals("start of result is at start", 0, ray1.distanceSquared(result.getFirst()), 0);
632 assertEquals("end of result is at start", 0, ray3.distanceSquared(result.getLast()), 0);
633 result = Bezier.cubic(4, ray1, cp1, cp2, ray3);
634 assertEquals("size should be 4", 4, result.size());
635 assertEquals("start of result is at start", 0, ray1.distanceSquared(result.getFirst()), 0);
636 assertEquals("end of result is at start", 0, ray3.distanceSquared(result.getLast()), 0);
637
638 try
639 {
640 Bezier.cubic(1, ray1, cp1, cp2, ray3);
641 fail("Cannot construct a Bezier approximation that has only one point");
642 }
643 catch (DrawRuntimeException dre)
644 {
645
646 }
647
648 }
649
650 }