1 package org.djutils.draw.line;
2
3 import static org.junit.Assert.assertEquals;
4 import static org.junit.Assert.assertFalse;
5 import static org.junit.Assert.assertNotEquals;
6 import static org.junit.Assert.assertNotNull;
7 import static org.junit.Assert.assertNull;
8 import static org.junit.Assert.assertTrue;
9 import static org.junit.Assert.fail;
10
11 import java.awt.geom.Path2D;
12 import java.util.ArrayList;
13 import java.util.Arrays;
14 import java.util.Iterator;
15 import java.util.List;
16 import java.util.NoSuchElementException;
17
18 import org.djutils.draw.DrawRuntimeException;
19 import org.djutils.draw.Transform2d;
20 import org.djutils.draw.bounds.Bounds2d;
21 import org.djutils.draw.line.PolyLine.TransitionFunction;
22 import org.djutils.draw.point.Point2d;
23 import org.djutils.draw.point.Point3d;
24 import org.djutils.exceptions.Try;
25 import org.junit.Test;
26
27
28
29
30
31
32
33
34
35
36 public class PolyLine2dTest
37 {
38
39
40
41
42
43 @Test
44 public final void constructorsTest() throws DrawRuntimeException
45 {
46 double[] values = { -999, 0, 99, 9999 };
47 Point2d[] points = new Point2d[0];
48 try
49 {
50 runConstructors(points);
51 fail("Should have thrown a DrawRuntimeException");
52 }
53 catch (DrawRuntimeException exception)
54 {
55
56 }
57 for (double x0 : values)
58 {
59 for (double y0 : values)
60 {
61 points = new Point2d[1];
62 points[0] = new Point2d(x0, y0);
63 try
64 {
65 runConstructors(points);
66 fail("Should have thrown a DrawRuntimeException");
67 }
68 catch (DrawRuntimeException exception)
69 {
70
71 }
72 for (double x1 : values)
73 {
74 for (double y1 : values)
75 {
76 points = new Point2d[2];
77 points[0] = new Point2d(x0, y0);
78 points[1] = new Point2d(x1, y1);
79 if (0 == points[0].distance(points[1]))
80 {
81 try
82 {
83 runConstructors(points);
84 fail("Should have thrown a DrawRuntimeException");
85 }
86 catch (DrawRuntimeException exception)
87 {
88
89 }
90 }
91 else
92 {
93 runConstructors(points);
94 for (double x2 : values)
95 {
96 for (double y2 : values)
97 {
98 points = new Point2d[3];
99 points[0] = new Point2d(x0, y0);
100 points[1] = new Point2d(x1, y1);
101 points[2] = new Point2d(x2, y2);
102 if (0 == points[1].distance(points[2]))
103 {
104 try
105 {
106 runConstructors(points);
107 fail("Should have thrown a DrawRuntimeException");
108 }
109 catch (DrawRuntimeException exception)
110 {
111
112 }
113 }
114 else
115 {
116 runConstructors(points);
117 }
118 }
119 }
120 }
121 }
122 }
123 }
124 }
125 }
126
127
128
129
130
131
132 private void runConstructors(final Point2d[] points) throws DrawRuntimeException
133 {
134 verifyPointsAndSegments(new PolyLine2d(points), points);
135 List<Point2d> list = new ArrayList<>();
136 for (int i = 0; i < points.length; i++)
137 {
138 list.add(points[i]);
139 }
140 PolyLine2d line = new PolyLine2d(list);
141 verifyPointsAndSegments(line, points);
142 verifyPointsAndSegments(new PolyLine2d(line.getPoints()), points);
143 assertEquals("length at index 0", 0.0, line.lengthAtIndex(0), 0);
144 double length = 0;
145 for (int i = 1; i < points.length; i++)
146 {
147 length += Math.sqrt(Math.pow(points[i].x - points[i - 1].x, 2) + Math.pow(points[i].y - points[i - 1].y, 2));
148 assertEquals("length at index", length, line.lengthAtIndex(i), 0.0001);
149 }
150 assertEquals("length", length, line.getLength(), 10 * Math.ulp(length));
151
152 assertEquals("size", points.length, line.size());
153
154 Bounds2d b2d = line.getBounds();
155 Bounds2d ref = new Bounds2d(points);
156 assertEquals("bounds is correct", ref, b2d);
157
158 try
159 {
160 line.get(-1);
161 fail("Negative index should have thrown an IndexOutOfBoundsException");
162 }
163 catch (IndexOutOfBoundsException ioobe)
164 {
165
166 }
167
168 try
169 {
170 line.get(line.size() + 1);
171 fail("Too large index should have thrown an IndexOutOfBoundsException");
172 }
173 catch (IndexOutOfBoundsException ioobe)
174 {
175
176 }
177
178 try
179 {
180 new PolyLine2d((List<Point2d>) null);
181 fail("null list should have thrown a NullPointerException");
182 }
183 catch (NullPointerException npe)
184 {
185
186 }
187
188 int horizontalMoves = 0;
189 Path2D path = new Path2D.Double();
190 path.moveTo(points[0].x, points[0].y);
191
192 for (int i = 1; i < points.length; i++)
193 {
194
195 if (points[i].x != points[i - 1].x || points[i].y != points[i - 1].y)
196 {
197 path.lineTo(points[i].x, points[i].y);
198 horizontalMoves++;
199 }
200 }
201
202 try
203 {
204 new PolyLine2d((Path2D) null);
205 fail("null path should have thrown a NullPointerException");
206 }
207 catch (NullPointerException npe)
208 {
209
210 }
211
212 try
213 {
214 line = new PolyLine2d(path);
215 if (0 == horizontalMoves)
216 {
217 fail("Construction of Line2d from path with degenerate projection should have failed");
218 }
219 assertEquals("number of points should match", horizontalMoves + 1, line.size());
220 int indexInLine = 0;
221 for (int i = 0; i < points.length; i++)
222 {
223 if (i > 0 && (points[i].x != points[i - 1].x || points[i].y != points[i - 1].y))
224 {
225 indexInLine++;
226 }
227 assertEquals("x in line", points[i].x, line.get(indexInLine).x, 0.001);
228 assertEquals("y in line", points[i].y, line.get(indexInLine).y, 0.001);
229 }
230 }
231 catch (DrawRuntimeException e)
232 {
233 if (0 != horizontalMoves)
234 {
235 fail("Construction of Line2d from path with non-degenerate projection should not have failed");
236 }
237 }
238 }
239
240
241
242
243
244 @Test
245 public void testPathWithClose() throws DrawRuntimeException
246 {
247 Path2D path = new Path2D.Double();
248 path.moveTo(1, 2);
249 path.lineTo(4, 5);
250 path.lineTo(4, 8);
251 path.closePath();
252 PolyLine2d line = new PolyLine2d(path);
253 assertEquals("line has 4 points", 4, line.size());
254 assertEquals("first point equals last point", line.getFirst(), line.getLast());
255
256 path = new Path2D.Double();
257 path.moveTo(1, 2);
258 path.lineTo(4, 5);
259 path.lineTo(1, 2);
260 path.closePath();
261 line = new PolyLine2d(path);
262 assertEquals("line has 4 points", 3, line.size());
263 assertEquals("first point equals last point", line.getFirst(), line.getLast());
264 path = new Path2D.Double();
265 path.moveTo(1, 2);
266 path.lineTo(4, 5);
267 path.lineTo(4, 8);
268 path.curveTo(1, 2, 3, 4, 5, 6);
269 try
270 {
271 new PolyLine2d(path);
272 fail("unsupported SEG_CUBICTO should have thrown an exception");
273 }
274 catch (DrawRuntimeException dre)
275 {
276
277 }
278 }
279
280
281
282
283
284
285 @Test
286 public void testConstructors() throws DrawRuntimeException, DrawRuntimeException
287 {
288 runConstructors(new Point2d[] { new Point2d(1.2, 3.4), new Point2d(2.3, 4.5), new Point2d(3.4, 5.6) });
289
290 Try.testFail(new Try.Execution()
291 {
292 @Override
293 public void execute() throws Throwable
294 {
295 new PolyLine2d(new double[] { 1, 2, 3 }, new double[] { 4, 5 });
296 }
297 }, "double arrays of unequal length should have thrown a DrawRuntimeException", DrawRuntimeException.class);
298
299 Try.testFail(new Try.Execution()
300 {
301 @Override
302 public void execute() throws Throwable
303 {
304 new PolyLine2d(new double[] { 1, 2 }, new double[] { 3, 4, 5 });
305 }
306 }, "double arrays of unequal length should have thrown a DrawRuntimeException", DrawRuntimeException.class);
307
308 Try.testFail(new Try.Execution()
309 {
310 @Override
311 public void execute() throws Throwable
312 {
313 new PolyLine2d(null, new double[] { 1, 2 });
314 }
315 }, "null double array should have thrown a NullPointerException", NullPointerException.class);
316
317 Try.testFail(new Try.Execution()
318 {
319 @Override
320 public void execute() throws Throwable
321 {
322 new PolyLine2d(new double[] { 1, 2 }, null);
323 }
324 }, "null double array should have thrown a NullPointerException", NullPointerException.class);
325
326 Try.testFail(new Try.Execution()
327 {
328 @Override
329 public void execute() throws Throwable
330 {
331 new PolyLine2d((List<Point2d>) null);
332 }
333 }, "null list should have thrown a nullPointerException", NullPointerException.class);
334
335 List<Point2d> shortList = new ArrayList<>();
336 Try.testFail(new Try.Execution()
337 {
338 @Override
339 public void execute() throws Throwable
340 {
341 new PolyLine2d(shortList);
342 }
343 }, "empty list should have thrown a DrawRuntimeException", DrawRuntimeException.class);
344
345 shortList.add(new Point2d(1, 2));
346 Try.testFail(new Try.Execution()
347 {
348 @Override
349 public void execute() throws Throwable
350 {
351 new PolyLine2d(shortList);
352 }
353 }, "one-point list should have thrown a DrawRuntimeException", DrawRuntimeException.class);
354
355 Point2d p1 = new Point2d(1, 2);
356 Point2d p2 = new Point2d(3, 4);
357 PolyLine2d pl = new PolyLine2d(p1, p2);
358 assertEquals("two points", 2, pl.size());
359 assertEquals("p1", p1, pl.get(0));
360 assertEquals("p2", p2, pl.get(1));
361
362 pl = new PolyLine2d(p1, p2, (Point2d[]) null);
363 assertEquals("two points", 2, pl.size());
364 assertEquals("p1", p1, pl.get(0));
365 assertEquals("p2", p2, pl.get(1));
366
367 pl = new PolyLine2d(p1, p2, new Point2d[0]);
368 assertEquals("two points", 2, pl.size());
369 assertEquals("p1", p1, pl.get(0));
370 assertEquals("p2", p2, pl.get(1));
371
372 Try.testFail(new Try.Execution()
373 {
374 @Override
375 public void execute() throws Throwable
376 {
377 new PolyLine2d(new Point2d[] {});
378 }
379 }, "empty array should have thrown a DrawRuntimeException", DrawRuntimeException.class);
380
381 Try.testFail(new Try.Execution()
382 {
383 @Override
384 public void execute() throws Throwable
385 {
386 new PolyLine2d(new Point2d[] { new Point2d(1, 2) });
387 }
388 }, "single point should have thrown a DrawRuntimeException", DrawRuntimeException.class);
389
390 Try.testFail(new Try.Execution()
391 {
392 @Override
393 public void execute() throws Throwable
394 {
395 new PolyLine2d(new Point2d[] { new Point2d(1, 2), new Point2d(1, 2) });
396 }
397 }, "duplicate point should have thrown a DrawRuntimeException", DrawRuntimeException.class);
398
399 Try.testFail(new Try.Execution()
400 {
401 @Override
402 public void execute() throws Throwable
403 {
404 new PolyLine2d(new Point2d[] { new Point2d(1, 2), new Point2d(1, 2), new Point2d(3, 4) });
405 }
406 }, "duplicate point should have thrown a DrawRuntimeException", DrawRuntimeException.class);
407
408 Try.testFail(new Try.Execution()
409 {
410 @Override
411 public void execute() throws Throwable
412 {
413 new PolyLine2d(new Point2d[] { new Point2d(-1, -2), new Point2d(1, 2), new Point2d(1, 2), new Point2d(3, 4) });
414 }
415 }, "duplicate point should have thrown a DrawRuntimeException", DrawRuntimeException.class);
416 }
417
418
419
420
421
422
423 @SuppressWarnings("unlikely-arg-type")
424 @Test
425 public void testOtherMethods() throws NullPointerException, DrawRuntimeException
426 {
427 Point2d[] array = new Point2d[] { new Point2d(1, 2), new Point2d(3, 4), new Point2d(3.2, 4.1), new Point2d(5, 6) };
428 PolyLine2d line = new PolyLine2d(Arrays.stream(array).iterator());
429 assertEquals("size", array.length, line.size());
430 for (int i = 0; i < array.length; i++)
431 {
432 assertEquals("i-th point", array[i], line.get(i));
433 }
434 int nextIndex = 0;
435 for (Iterator<Point2d> iterator = line.getPoints(); iterator.hasNext();)
436 {
437 assertEquals("i-th point from line iterator", array[nextIndex++], iterator.next());
438 }
439 assertEquals("iterator returned all points", array.length, nextIndex);
440
441 PolyLine2d filtered = line.noiseFilteredLine(0.0);
442 assertEquals("filtered with 0 tolerance returns line", line, filtered);
443 filtered = line.noiseFilteredLine(0.01);
444 assertEquals("filtered with very low tolerance returns line", line, filtered);
445 filtered = line.noiseFilteredLine(0.5);
446 assertEquals("size of filtered line is 3", 3, filtered.size());
447 assertEquals("first point of filtered line matches", line.getFirst(), filtered.getFirst());
448 assertEquals("last point of filtered line matches", line.getLast(), filtered.getLast());
449 assertEquals("mid point of filtered line is point 1 of unfiltered line", line.get(1), filtered.get(1));
450 filtered = line.noiseFilteredLine(10);
451 assertEquals("size of filtered line is 2", 2, filtered.size());
452 assertEquals("first point of filtered line matches", line.getFirst(), filtered.getFirst());
453 assertEquals("last point of filtered line matches", line.getLast(), filtered.getLast());
454
455 array = new Point2d[] { new Point2d(1, 2), new Point2d(3, 4), new Point2d(3.2, 4.1), new Point2d(1, 2) };
456 line = new PolyLine2d(Arrays.stream(array).iterator());
457 filtered = line.noiseFilteredLine(10);
458 assertEquals("size of filtered line is 3", 3, filtered.size());
459 assertEquals("first point of filtered line matches", line.getFirst(), filtered.getFirst());
460 assertEquals("last point of filtered line matches", line.getLast(), filtered.getLast());
461 assertEquals("mid point of filtered line is point 1 of unfiltered line", line.get(1), filtered.get(1));
462
463 array = new Point2d[] { new Point2d(1, 2), new Point2d(3, 4), new Point2d(1.1, 2.1), new Point2d(1, 2) };
464 line = new PolyLine2d(Arrays.stream(array).iterator());
465 filtered = line.noiseFilteredLine(0.5);
466 assertEquals("size of filtered line is 3", 3, filtered.size());
467 assertEquals("first point of filtered line matches", line.getFirst(), filtered.getFirst());
468 assertEquals("last point of filtered line matches", line.getLast(), filtered.getLast());
469 assertEquals("mid point of filtered line is point 1 of unfiltered line", line.get(1), filtered.get(1));
470
471 array = new Point2d[] { new Point2d(1, 2), new Point2d(3, 4) };
472 line = new PolyLine2d(Arrays.stream(array).iterator());
473 filtered = line.noiseFilteredLine(10);
474 assertEquals("Filtering a two-point line returns that line", line, filtered);
475
476 array = new Point2d[] { new Point2d(1, 2), new Point2d(1, 2), new Point2d(1, 2), new Point2d(3, 4) };
477 line = new PolyLine2d(true, array);
478 assertEquals("cleaned line has 2 points", 2, line.size());
479 assertEquals("first point", array[0], line.getFirst());
480 assertEquals("last point", array[array.length - 1], line.getLast());
481
482 array = new Point2d[] { new Point2d(1, 2), new Point2d(1, 2), new Point2d(3, 4), new Point2d(3, 4) };
483 line = new PolyLine2d(true, array);
484 assertEquals("cleaned line has 2 points", 2, line.size());
485 assertEquals("first point", array[0], line.getFirst());
486 assertEquals("last point", array[array.length - 1], line.getLast());
487
488 array = new Point2d[] { new Point2d(0, -1), new Point2d(1, 2), new Point2d(1, 2), new Point2d(3, 4) };
489 line = new PolyLine2d(true, array);
490 assertEquals("cleaned line has 2 points", 3, line.size());
491 assertEquals("first point", array[0], line.getFirst());
492 assertEquals("last point", array[array.length - 1], line.getLast());
493
494 array = new Point2d[] { new Point2d(0, -1), new Point2d(1, 2), new Point2d(1, 2), new Point2d(1, 2),
495 new Point2d(3, 4) };
496 line = new PolyLine2d(true, array);
497 assertEquals("cleaned line has 3 points", 3, line.size());
498 assertEquals("first point", array[0], line.getFirst());
499 assertEquals("mid point", array[1], line.get(1));
500 assertEquals("last point", array[array.length - 1], line.getLast());
501
502 Try.testFail(new Try.Execution()
503 {
504 @Override
505 public void execute() throws Throwable
506 {
507 new PolyLine2d(true, new Point2d[0]);
508 }
509 }, "Too short array should have thrown a DrawRuntimeException", DrawRuntimeException.class);
510
511 Try.testFail(new Try.Execution()
512 {
513 @Override
514 public void execute() throws Throwable
515 {
516 new PolyLine2d(true, new Point2d[] { new Point2d(1, 2) });
517 }
518 }, "Too short array should have thrown a DrawRuntimeException", DrawRuntimeException.class);
519
520 Try.testFail(new Try.Execution()
521 {
522 @Override
523 public void execute() throws Throwable
524 {
525 new PolyLine2d(true, new Point2d[] { new Point2d(1, 2), new Point2d(1, 2) });
526 }
527 }, "All duplicate points in array should have thrown a DrawRuntimeException", DrawRuntimeException.class);
528
529 Try.testFail(new Try.Execution()
530 {
531 @Override
532 public void execute() throws Throwable
533 {
534 new PolyLine2d(true, new Point2d[] { new Point2d(1, 2), new Point2d(1, 2), new Point2d(1, 2) });
535 }
536 }, "All duplicate points in array should have thrown a DrawRuntimeException", DrawRuntimeException.class);
537
538 array = new Point2d[] { new Point2d(1, 2), new Point2d(4, 6), new Point2d(8, 9) };
539 line = new PolyLine2d(array);
540
541 try
542 {
543 line.getLocation(-0.1);
544 fail("negative location should have thrown a DrawRuntimeException");
545 }
546 catch (DrawRuntimeException dre)
547 {
548
549 }
550
551 double length = line.getLength();
552 assertEquals("Length of line is 10", 10, length, 0.000001);
553
554 try
555 {
556 line.getLocation(length + 0.1);
557 fail("location beyond length should have thrown a DrawRuntimeException");
558 }
559 catch (DrawRuntimeException dre)
560 {
561
562 }
563
564 try
565 {
566 line.getLocation(-0.1);
567 fail("negative location should have thrown a DrawRuntimeException");
568 }
569 catch (DrawRuntimeException dre)
570 {
571
572 }
573
574 assertEquals("Length of line is 10", 10, length, 0.000001);
575
576 try
577 {
578 line.getLocationFraction(1.1);
579 fail("location beyond length should have thrown a DrawRuntimeException");
580 }
581 catch (DrawRuntimeException de)
582 {
583
584 }
585
586 try
587 {
588 line.getLocationFraction(-0.1);
589 fail("negative location should have thrown a DrawRuntimeException");
590 }
591 catch (DrawRuntimeException dre)
592 {
593
594 }
595
596 for (double position : new double[] { -1, 0, 2.5, 4.9, 5.1, 7.5, 9.9, 10, 11 })
597 {
598 Ray2d ray = line.getLocationExtended(position);
599 if (position < 5)
600 {
601 Ray2d expected = new Ray2d(array[0].interpolate(array[1], position / 5), Math.atan2(4, 3));
602 assertTrue("interpolated/extrapolated point", expected.epsilonEquals(ray, 0.0001, 0.00001));
603 }
604 else
605 {
606 Ray2d expected = new Ray2d(array[1].interpolate(array[2], (position - 5) / 5), Math.atan2(3, 4));
607 assertTrue("interpolated/extrapolated point", expected.epsilonEquals(ray, 0.0001, 0.00001));
608 }
609 ray = line.getLocationFractionExtended(position / line.getLength());
610 if (position < 5)
611 {
612 Ray2d expected = new Ray2d(array[0].interpolate(array[1], position / 5), Math.atan2(4, 3));
613 assertTrue("interpolated/extrapolated point", expected.epsilonEquals(ray, 0.0001, 0.00001));
614 }
615 else
616 {
617 Ray2d expected = new Ray2d(array[1].interpolate(array[2], (position - 5) / 5), Math.atan2(3, 4));
618 assertTrue("interpolated/extrapolated point", expected.epsilonEquals(ray, 0.0001, 0.00001));
619 }
620 }
621
622
623 array = new Point2d[] { new Point2d(1, 2), new Point2d(4, 6), new Point2d(8, 9) };
624 line = new PolyLine2d(array);
625
626 for (double x = -15; x <= 20; x++)
627 {
628 for (double y = -15; y <= 20; y++)
629 {
630 Point2d xy = new Point2d(x, y);
631
632 double result = line.projectOrthogonalFractional(xy);
633 if (!Double.isNaN(result))
634 {
635 assertTrue("result must be >= 0.0", result >= 0);
636 assertTrue("result must be <= 1.0", result <= 1.0);
637 Ray2d ray = line.getLocationFraction(result);
638 Point2d projected = line.projectOrthogonal(xy);
639 assertEquals("if fraction is between 0 and 1; projectOrthogonal yiels point at that fraction", ray.x,
640 projected.x, 00001);
641 assertEquals("if fraction is between 0 and 1; projectOrthogonal yiels point at that fraction", ray.y,
642 projected.y, 00001);
643 }
644 else
645 {
646 assertNull("point projects outside line", line.projectOrthogonal(xy));
647 }
648 result = line.projectOrthogonalFractionalExtended(xy);
649 if (!Double.isNaN(result))
650 {
651 Point2d resultPoint = line.getLocationFractionExtended(result);
652 if (result >= 0.0 && result <= 1.0)
653 {
654 Point2d closestPointOnLine = line.closestPointOnPolyLine(xy);
655 assertEquals("resultPoint is equal to closestPoint", resultPoint, closestPointOnLine);
656 assertEquals("getLocationFraction returns same as getLocationfractionExtended", resultPoint,
657 line.getLocationFraction(result));
658 }
659 else
660 {
661 try
662 {
663 line.getLocationFraction(result);
664 fail("illegal fraction should have thrown a DrawRuntimeException");
665 }
666 catch (DrawRuntimeException dre)
667 {
668
669 }
670 if (result < 0)
671 {
672 assertEquals("resultPoint lies on extention of start segment",
673 resultPoint.distance(line.get(1)) - resultPoint.distance(line.getFirst()),
674 line.getFirst().distance(line.get(1)), 0.0001);
675 }
676 else
677 {
678
679 assertEquals("resultPoint lies on extention of end segment",
680 resultPoint.distance(line.get(line.size() - 2)) - resultPoint.distance(line.getLast()),
681 line.getLast().distance(line.get(line.size() - 2)), 0.0001);
682 }
683 }
684 }
685 else
686 {
687 assertNull("point projects outside extended line", line.projectOrthogonalExtended(xy));
688 Point2d closestPointOnLine = line.closestPointOnPolyLine(xy);
689 assertNotNull("closest point is never null", closestPointOnLine);
690 boolean found = false;
691 for (int index = 0; index < line.size(); index++)
692 {
693 Point2d linePoint = line.get(index);
694 if (linePoint.x == closestPointOnLine.x && linePoint.y == closestPointOnLine.y)
695 {
696 found = true;
697 }
698 }
699 assertTrue("closestPointOnLine is one of the construction points of the line", found);
700 }
701 Point2d closestPointOnLine = line.closestPointOnPolyLine(xy);
702 assertNotNull("closest point is never null", closestPointOnLine);
703 }
704 }
705 Point2d toleranceResultPoint = line.getLocationFraction(-0.01, 0.01);
706 assertEquals("tolerance result matches extended fraction result", line.getLocationFraction(0), toleranceResultPoint);
707 toleranceResultPoint = line.getLocationFraction(1.01, 0.01);
708 assertEquals("tolerance result matches extended fraction result", line.getLocationFraction(1), toleranceResultPoint);
709
710 try
711 {
712 line.getLocationFraction(-.011, 0.01);
713 fail("fraction outside tolerance should have thrown a DrawRuntimeException");
714 }
715 catch (DrawRuntimeException dre)
716 {
717
718 }
719
720 try
721 {
722 line.getLocationFraction(1.011, 0.01);
723 fail("fraction outside tolerance should have thrown a DrawRuntimeException");
724 }
725 catch (DrawRuntimeException dre)
726 {
727
728 }
729
730
731 array = new Point2d[] { new Point2d(1, 2), new Point2d(4, 6), new Point2d(8, 9) };
732 line = new PolyLine2d(array);
733 length = line.getLength();
734 for (double to : new double[] { -10, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 20 })
735 {
736 if (to <= 0 || to > length)
737 {
738 try
739 {
740 line.truncate(to);
741 fail("illegal truncate should have thrown a DrawRuntimeException");
742 }
743 catch (DrawRuntimeException dre)
744 {
745
746 }
747 }
748 else
749 {
750 PolyLine2d truncated = line.truncate(to);
751 assertEquals("truncated line start with start point of line", line.getFirst(), truncated.getFirst());
752 assertEquals("Length of truncated line is truncate position", to, truncated.getLength(), 0.0001);
753 }
754 for (double from : new double[] { -10, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 20 })
755 {
756 if (from >= to || from < 0 || to > length)
757 {
758 try
759 {
760 line.extract(from, to);
761 fail("Illegal range should have thrown a DrawRuntimeException");
762 }
763 catch (DrawRuntimeException dre)
764 {
765
766 }
767 }
768 else
769 {
770 PolyLine2d fragment = line.extract(from, to);
771 Point2d fromPoint = line.getLocation(from);
772 assertTrue("fragment starts at from", fromPoint.epsilonEquals(fragment.getFirst(), 0.00001));
773 Point2d toPoint = line.getLocation(to);
774 assertTrue("fragment ends at to", toPoint.epsilonEquals(fragment.getLast(), 0.00001));
775 assertEquals("Length of fragment", to - from, fragment.getLength(), 0.0001);
776 if (from == 0)
777 {
778 assertEquals("fragment starts at begin of line", line.getFirst(), fragment.getFirst());
779 }
780 if (to == length)
781 {
782 assertEquals("fragment ends at end of line", line.getLast(), fragment.getLast());
783 }
784 }
785 }
786 }
787 try
788 {
789 line.extract(Double.NaN, 10.0);
790 fail("NaN value should have thrown a DrawRuntimeException");
791 }
792 catch (DrawRuntimeException dre)
793 {
794
795 }
796
797 try
798 {
799 line.extract(0.0, Double.NaN);
800 fail("NaN value should have thrown a DrawRuntimeException");
801 }
802 catch (DrawRuntimeException dre)
803 {
804
805 }
806
807
808 assertNotEquals("hash code takes x coordinate of first point into account",
809 new PolyLine2d(new Point2d(0, 0), new Point2d(1, 1)).hashCode(),
810 new PolyLine2d(new Point2d(1, 0), new Point2d(1, 1)).hashCode());
811 assertNotEquals("hash code takes y coordinate of first point into account",
812 new PolyLine2d(new Point2d(0, 0), new Point2d(1, 1)).hashCode(),
813 new PolyLine2d(new Point2d(0, 1), new Point2d(1, 1)).hashCode());
814 assertNotEquals("hash code takes x coordinate of second point into account",
815 new PolyLine2d(new Point2d(0, 0), new Point2d(1, 1)).hashCode(),
816 new PolyLine2d(new Point2d(0, 0), new Point2d(2, 1)).hashCode());
817 assertNotEquals("hash code takes y coordinate of second point into account",
818 new PolyLine2d(new Point2d(0, 0), new Point2d(1, 1)).hashCode(),
819 new PolyLine2d(new Point2d(0, 0), new Point2d(1, 2)).hashCode());
820
821
822 assertTrue("line is equal to itself", line.equals(line));
823 assertFalse("line is not equal to a different line",
824 line.equals(new PolyLine2d(new Point2d(123, 456), new Point2d(789, 101112))));
825 assertFalse("line is not equal to null", line.equals(null));
826 assertFalse("line is not equal to a different kind of object", line.equals("unlikely"));
827 assertTrue("Line is equal to line from same set of points", line.equals(new PolyLine2d(line.getPoints())));
828
829 Point2d[] otherArray = Arrays.copyOf(array, array.length);
830 otherArray[otherArray.length - 1] =
831 new Point2d(otherArray[otherArray.length - 1].x, otherArray[otherArray.length - 1].y + 5);
832 PolyLine2d other = new PolyLine2d(otherArray);
833 assertFalse("PolyLine2d that differs in y of last point is different", line.equals(other));
834 }
835
836
837
838
839
840 @Test
841 public final void concatenateTest() throws DrawRuntimeException
842 {
843 Point2d p0 = new Point2d(1.1, 2.2);
844 Point2d p1 = new Point2d(2.1, 2.2);
845 Point2d p2 = new Point2d(3.1, 2.2);
846 Point2d p3 = new Point2d(4.1, 2.2);
847 Point2d p4 = new Point2d(5.1, 2.2);
848 Point2d p5 = new Point2d(6.1, 2.2);
849
850 PolyLine2d l0 = new PolyLine2d(p0, p1, p2);
851 PolyLine2d l1 = new PolyLine2d(p2, p3);
852 PolyLine2d l2 = new PolyLine2d(p3, p4, p5);
853 PolyLine2d ll = PolyLine2d.concatenate(l0, l1, l2);
854 assertEquals("size is 6", 6, ll.size());
855 assertEquals("point 0 is p0", p0, ll.get(0));
856 assertEquals("point 1 is p1", p1, ll.get(1));
857 assertEquals("point 2 is p2", p2, ll.get(2));
858 assertEquals("point 3 is p3", p3, ll.get(3));
859 assertEquals("point 4 is p4", p4, ll.get(4));
860 assertEquals("point 5 is p5", p5, ll.get(5));
861
862 ll = PolyLine2d.concatenate(l1);
863 assertEquals("size is 2", 2, ll.size());
864 assertEquals("point 0 is p2", p2, ll.get(0));
865 assertEquals("point 1 is p3", p3, ll.get(1));
866
867 try
868 {
869 PolyLine2d.concatenate(l0, l2);
870 fail("Gap should have throw a DrawRuntimeException");
871 }
872 catch (DrawRuntimeException dre)
873 {
874
875 }
876 try
877 {
878 PolyLine2d.concatenate();
879 fail("concatenate of empty list should have thrown a DrawRuntimeException");
880 }
881 catch (DrawRuntimeException dre)
882 {
883
884 }
885
886
887 PolyLine2d thirdLine = new PolyLine2d(p4, p5);
888 for (double tolerance : new double[] { 0.1, 0.01, 0.001, 0.0001, 0.00001 })
889 {
890 for (double actualError : new double[] { tolerance * 0.9, tolerance * 1.1 })
891 {
892 int maxDirection = 10;
893 for (int direction = 0; direction < maxDirection; direction++)
894 {
895 double dx = actualError * Math.cos(Math.PI * 2 * direction / maxDirection);
896 double dy = actualError * Math.sin(Math.PI * 2 * direction / maxDirection);
897 PolyLine2d otherLine = new PolyLine2d(new Point2d(p2.x + dx, p2.y + dy), p3, p4);
898 if (actualError < tolerance)
899 {
900 try
901 {
902 PolyLine2d.concatenate(tolerance, l0, otherLine);
903 }
904 catch (DrawRuntimeException dre)
905 {
906 PolyLine2d.concatenate(tolerance, l0, otherLine);
907 fail("concatenation with error " + actualError + " and tolerance " + tolerance
908 + " should not have failed");
909 }
910 try
911 {
912 PolyLine2d.concatenate(tolerance, l0, otherLine, thirdLine);
913 }
914 catch (DrawRuntimeException dre)
915 {
916 fail("concatenation with error " + actualError + " and tolerance " + tolerance
917 + " should not have failed");
918 }
919 }
920 else
921 {
922 try
923 {
924 PolyLine2d.concatenate(tolerance, l0, otherLine);
925 }
926 catch (DrawRuntimeException dre)
927 {
928
929 }
930 try
931 {
932 PolyLine2d.concatenate(tolerance, l0, otherLine, thirdLine);
933 }
934 catch (DrawRuntimeException dre)
935 {
936
937 }
938 }
939 }
940 }
941 }
942 }
943
944
945
946
947
948 @Test
949 public void testOffsetLine() throws DrawRuntimeException
950 {
951 for (Point2d[] points : new Point2d[][] { { new Point2d(1, 2), new Point2d(3, 50) },
952 { new Point2d(-40, -20), new Point2d(5, -2), new Point2d(3, 50) },
953 { new Point2d(-40, -20), new Point2d(5, -2), new Point2d(3, -50) } })
954 {
955 for (double angle = 0; angle < 2 * Math.PI; angle += Math.PI / 360)
956 {
957 Transform2d transform = new Transform2d().rotation(angle);
958 Point2d[] transformed = new Point2d[points.length];
959 for (int index = 0; index < points.length; index++)
960 {
961 transformed[index] = transform.transform(points[index]);
962 }
963 final PolyLine2d line = new PolyLine2d(transformed);
964
965 Try.testFail(new Try.Execution()
966 {
967 @Override
968 public void execute() throws Throwable
969 {
970 line.offsetLine(Double.NaN);
971 }
972 }, "NaN offset should have thrown an IllegalArgumentException", IllegalArgumentException.class);
973
974 assertEquals("offset 0 yields the reference line", line, line.offsetLine(0));
975
976 for (double offset : new double[] { 1, 10, 0.1, -0.1, -10 })
977 {
978 PolyLine2d offsetLine = line.offsetLine(offset);
979
980 if (points.length == 2)
981 {
982 assertEquals("two-point line should have a two-point offset line", 2, offsetLine.size());
983 assertEquals("length of offset line of two-point reference line equals length of reference line",
984 line.getLength(), offsetLine.getLength(), 0.01);
985 }
986 assertEquals("offset at start", Math.abs(offset), line.getFirst().distance(offsetLine.getFirst()), 0.01);
987 assertEquals("offset at end", Math.abs(offset), line.getLast().distance(offsetLine.getLast()), 0.01);
988
989 assertEquals("offset to the left vs to the right differs by twice the offset", Math.abs(2 * offset),
990 offsetLine.getFirst().distance(line.offsetLine(-offset).getFirst()), 0.001);
991
992
993 assertEquals("projection of first point of line onto offset line is (almost) first point of offset line", 0,
994 offsetLine.getLocationExtended(
995 offsetLine.projectOrthogonalFractionalExtended(line.getFirst()) * offsetLine.getLength())
996 .distance(offsetLine.getFirst()),
997 0.01);
998 double fraction = offsetLine.projectOrthogonalFractionalExtended(line.getLast());
999 assertEquals("fraction should be 1 with maximum error a few ULP", 1, fraction, 0.000001);
1000 if (fraction > 1.0)
1001 {
1002 fraction = 1.0;
1003 }
1004 assertEquals("projection of last point of line onto offset line is (almost) last point of offset line", 0,
1005 offsetLine.getLocation(fraction * offsetLine.getLength()).distance(offsetLine.getLast()), 0.01);
1006 assertEquals("projection of first point of offset line onto line is (almost) first point of line", 0,
1007 line.getLocationExtended(
1008 line.projectOrthogonalFractionalExtended(offsetLine.getFirst()) * line.getLength())
1009 .distance(line.getFirst()),
1010 0.01);
1011 fraction = line.projectOrthogonalFractionalExtended(offsetLine.getLast());
1012 assertEquals("fraction should be 1 with maximum error a few ULP", 1, fraction, 0.000001);
1013 if (fraction > 1.0)
1014 {
1015 fraction = 1.0;
1016 }
1017 assertEquals("projection of last point of offset line onto line is (almost) last point of line", 0,
1018 line.getLocation(fraction * line.getLength()).distance(line.getLast()), 0.01);
1019 }
1020 }
1021 }
1022
1023 final PolyLine2d line = new PolyLine2d(new Point2d(1, 2), new Point2d(3, 4));
1024 Try.testFail(new Try.Execution()
1025 {
1026 @Override
1027 public void execute() throws Throwable
1028 {
1029 line.offsetLine(1, 0, PolyLine.DEFAULT_OFFSET_MINIMUM_FILTER_VALUE,
1030 PolyLine.DEFAULT_OFFSET_MAXIMUM_FILTER_VALUE, PolyLine.DEFAULT_OFFSET_FILTER_RATIO,
1031 PolyLine.DEFAULT_OFFSET_PRECISION);
1032 }
1033 }, "zero circle precision should have thrown an IllegalArgumentException", IllegalArgumentException.class);
1034
1035 Try.testFail(new Try.Execution()
1036 {
1037 @Override
1038 public void execute() throws Throwable
1039 {
1040 line.offsetLine(1, Double.NaN, PolyLine.DEFAULT_OFFSET_MINIMUM_FILTER_VALUE,
1041 PolyLine.DEFAULT_OFFSET_MAXIMUM_FILTER_VALUE, PolyLine.DEFAULT_OFFSET_FILTER_RATIO,
1042 PolyLine.DEFAULT_OFFSET_PRECISION);
1043 }
1044 }, "NaN circle precision should have thrown an IllegalArgumentException", IllegalArgumentException.class);
1045
1046 Try.testFail(new Try.Execution()
1047 {
1048 @Override
1049 public void execute() throws Throwable
1050 {
1051 line.offsetLine(1, PolyLine.DEFAULT_CIRCLE_PRECISION, 0, PolyLine.DEFAULT_OFFSET_MAXIMUM_FILTER_VALUE,
1052 PolyLine.DEFAULT_OFFSET_FILTER_RATIO, PolyLine.DEFAULT_OFFSET_PRECISION);
1053 }
1054 }, "zero offsetMinimumFilterValue should have thrown an IllegalArgumentException", IllegalArgumentException.class);
1055
1056 Try.testFail(new Try.Execution()
1057 {
1058 @Override
1059 public void execute() throws Throwable
1060 {
1061 line.offsetLine(1, PolyLine.DEFAULT_CIRCLE_PRECISION, Double.NaN, PolyLine.DEFAULT_OFFSET_MAXIMUM_FILTER_VALUE,
1062 PolyLine.DEFAULT_OFFSET_FILTER_RATIO, PolyLine.DEFAULT_OFFSET_PRECISION);
1063 }
1064 }, "NaN offsetMinimumFilterValue should have thrown an IllegalArgumentException", IllegalArgumentException.class);
1065
1066 Try.testFail(new Try.Execution()
1067 {
1068 @Override
1069 public void execute() throws Throwable
1070 {
1071 line.offsetLine(1, PolyLine.DEFAULT_CIRCLE_PRECISION, PolyLine.DEFAULT_OFFSET_MAXIMUM_FILTER_VALUE,
1072 PolyLine.DEFAULT_OFFSET_MAXIMUM_FILTER_VALUE, PolyLine.DEFAULT_OFFSET_FILTER_RATIO,
1073 PolyLine.DEFAULT_OFFSET_PRECISION);
1074 }
1075 }, "offsetMinimumFilterValue not less than offsetMaximumFilterValue should have thrown an IllegalArgumentException",
1076 IllegalArgumentException.class);
1077
1078 Try.testFail(new Try.Execution()
1079 {
1080 @Override
1081 public void execute() throws Throwable
1082 {
1083 line.offsetLine(1, PolyLine.DEFAULT_CIRCLE_PRECISION, PolyLine.DEFAULT_OFFSET_MINIMUM_FILTER_VALUE, 0,
1084 PolyLine.DEFAULT_OFFSET_FILTER_RATIO, PolyLine.DEFAULT_OFFSET_PRECISION);
1085 }
1086 }, "zero offsetMaximumfilterValue should have thrown an IllegalArgumentException", IllegalArgumentException.class);
1087
1088 Try.testFail(new Try.Execution()
1089 {
1090 @Override
1091 public void execute() throws Throwable
1092 {
1093 line.offsetLine(1, PolyLine.DEFAULT_CIRCLE_PRECISION, PolyLine.DEFAULT_OFFSET_MINIMUM_FILTER_VALUE, Double.NaN,
1094 PolyLine.DEFAULT_OFFSET_FILTER_RATIO, PolyLine.DEFAULT_OFFSET_PRECISION);
1095 }
1096 }, "NaN offsetMaximumfilterValue should have thrown an IllegalArgumentException", IllegalArgumentException.class);
1097
1098 Try.testFail(new Try.Execution()
1099 {
1100 @Override
1101 public void execute() throws Throwable
1102 {
1103 line.offsetLine(1, PolyLine.DEFAULT_CIRCLE_PRECISION, PolyLine.DEFAULT_OFFSET_MINIMUM_FILTER_VALUE,
1104 PolyLine.DEFAULT_OFFSET_MAXIMUM_FILTER_VALUE, 0, PolyLine.DEFAULT_OFFSET_PRECISION);
1105 }
1106 }, "zero offsetFilterRatio should have thrown an IllegalArgumentException", IllegalArgumentException.class);
1107
1108 Try.testFail(new Try.Execution()
1109 {
1110 @Override
1111 public void execute() throws Throwable
1112 {
1113 line.offsetLine(1, PolyLine.DEFAULT_CIRCLE_PRECISION, PolyLine.DEFAULT_OFFSET_MINIMUM_FILTER_VALUE,
1114 PolyLine.DEFAULT_OFFSET_MAXIMUM_FILTER_VALUE, Double.NaN, PolyLine.DEFAULT_OFFSET_PRECISION);
1115 }
1116 }, "NaN offsetFilterRatio should have thrown an IllegalArgumentException", IllegalArgumentException.class);
1117
1118 Try.testFail(new Try.Execution()
1119 {
1120 @Override
1121 public void execute() throws Throwable
1122 {
1123 line.offsetLine(1, PolyLine.DEFAULT_CIRCLE_PRECISION, PolyLine.DEFAULT_OFFSET_MINIMUM_FILTER_VALUE,
1124 PolyLine.DEFAULT_OFFSET_MAXIMUM_FILTER_VALUE, PolyLine.DEFAULT_OFFSET_FILTER_RATIO, 0);
1125 }
1126 }, "zero offsetPrecision should have thrown an IllegalArgumentException", IllegalArgumentException.class);
1127
1128 Try.testFail(new Try.Execution()
1129 {
1130 @Override
1131 public void execute() throws Throwable
1132 {
1133 line.offsetLine(1, PolyLine.DEFAULT_CIRCLE_PRECISION, PolyLine.DEFAULT_OFFSET_MINIMUM_FILTER_VALUE,
1134 PolyLine.DEFAULT_OFFSET_MAXIMUM_FILTER_VALUE, PolyLine.DEFAULT_OFFSET_FILTER_RATIO, Double.NaN);
1135 }
1136 }, "NaN offsetPrecision should have thrown an IllegalArgumentException", IllegalArgumentException.class);
1137 }
1138
1139
1140
1141
1142
1143 @Test
1144 public void testProjectRayTransition() throws DrawRuntimeException
1145 {
1146 List<Point2d> innerDesignLinePoints = new ArrayList<>();
1147 List<Point2d> outerDesignLinePoints = new ArrayList<>();
1148
1149 double innerRadius = 5;
1150
1151 double outerRadius = 8;
1152 for (int degree = 0; degree <= 90; degree++)
1153 {
1154 innerDesignLinePoints.add(new Point2d(innerRadius * Math.sin(Math.toRadians(degree)),
1155 innerRadius * Math.cos(Math.toRadians(degree))));
1156 outerDesignLinePoints.add(new Point2d(outerRadius * Math.sin(Math.toRadians(degree)),
1157 outerRadius * Math.cos(Math.toRadians(degree))));
1158 }
1159 PolyLine2d innerDesignLine = new PolyLine2d(innerDesignLinePoints);
1160 PolyLine2d outerDesignLine = new PolyLine2d(outerDesignLinePoints);
1161 List<Point2d> transitionLinePoints = new ArrayList<>();
1162 int degree = 0;
1163 Point2d prevPoint = innerDesignLinePoints.get(0);
1164 while (degree < 10)
1165 {
1166 double x = innerRadius * Math.sin(Math.toRadians(degree));
1167 double y = innerRadius * Math.cos(Math.toRadians(degree));
1168 double direction = prevPoint.directionTo(new Point2d(x, y));
1169 Ray2d ray = new Ray2d(x, y, direction);
1170 transitionLinePoints.add(ray);
1171 prevPoint = ray;
1172 degree++;
1173 }
1174 while (degree <= 80)
1175 {
1176 double phase = Math.PI * (degree - 10) / 70;
1177 double radius = innerRadius + (outerRadius - innerRadius) * (1 - Math.cos(phase) / 2 - 0.5);
1178 double x = radius * Math.sin(Math.toRadians(degree));
1179 double y = radius * Math.cos(Math.toRadians(degree));
1180 double direction = prevPoint.directionTo(new Point2d(x, y));
1181 Ray2d ray = new Ray2d(x, y, direction);
1182 transitionLinePoints.add(ray);
1183 prevPoint = ray;
1184 degree++;
1185 }
1186 while (degree < 90)
1187 {
1188 double x = outerRadius * Math.sin(Math.toRadians(degree));
1189 double y = outerRadius * Math.cos(Math.toRadians(degree));
1190 double direction = prevPoint.directionTo(new Point2d(x, y));
1191 Ray2d ray = new Ray2d(x, y, direction);
1192 transitionLinePoints.add(ray);
1193 prevPoint = ray;
1194 degree++;
1195 }
1196 PolyLine2d transitionLine = new PolyLine2d(transitionLinePoints);
1197
1198
1199
1200 List<Point2d> projections = new ArrayList<>();
1201 for (Iterator<Point2d> iterator = transitionLine.getPoints(); iterator.hasNext();)
1202 {
1203 Point2d p = iterator.next();
1204 if (p instanceof Ray2d)
1205 {
1206 Ray2d ray = (Ray2d) p;
1207 Point2d transitionLinePoint = new Point2d(ray.x, ray.y);
1208 projections.add(transitionLinePoint);
1209 double location = innerDesignLine.projectRay(ray);
1210 if (!Double.isNaN(location))
1211 {
1212 Point2d projection = innerDesignLine.getLocation(location);
1213 projections.add(new Point2d(projection.x, projection.y));
1214 projections.add(transitionLinePoint);
1215 }
1216 location = outerDesignLine.projectRay(ray);
1217 if (!Double.isNaN(location))
1218 {
1219 Point2d projection = outerDesignLine.getLocation(location);
1220 projections.add(new Point2d(projection.x, projection.y));
1221 projections.add(transitionLinePoint);
1222 }
1223 }
1224 }
1225
1226 Ray2d from = new Ray2d(outerDesignLine.get(10).x, outerDesignLine.get(10).y,
1227 outerDesignLine.get(10).directionTo(outerDesignLine.get(11)));
1228 Ray2d to = new Ray2d(innerDesignLine.get(80).x, innerDesignLine.get(80).y,
1229 innerDesignLine.get(80).directionTo(innerDesignLine.get(81)));
1230 transitionLine = Bezier.cubic(from, to);
1231
1232 projections = new ArrayList<>();
1233 Point2d prev = null;
1234 for (Iterator<Point2d> iterator = transitionLine.getPoints(); iterator.hasNext();)
1235 {
1236 Point2d p = iterator.next();
1237 if (prev != null)
1238 {
1239 Ray2d ray = new Ray2d(prev, prev.directionTo(p));
1240 Point2d transitionLinePoint = new Point2d(ray.x, ray.y);
1241 projections.add(transitionLinePoint);
1242 double location = innerDesignLine.projectRay(ray);
1243 if (!Double.isNaN(location))
1244 {
1245 innerDesignLine.getLocation(location);
1246 Point2d projection = innerDesignLine.getLocation(location);
1247 projections.add(new Point2d(projection.x, projection.y));
1248 projections.add(transitionLinePoint);
1249 }
1250 location = outerDesignLine.projectRay(ray);
1251 if (!Double.isNaN(location))
1252 {
1253 outerDesignLine.getLocation(location);
1254 Point2d projection = outerDesignLine.getLocation(location);
1255 projections.add(new Point2d(projection.x, projection.y));
1256 projections.add(transitionLinePoint);
1257 }
1258 }
1259 prev = p;
1260 }
1261
1262 }
1263
1264
1265
1266
1267 @Test
1268 public void testProjectRay()
1269 {
1270 PolyLine2d reference = new PolyLine2d(new Point2d(0, 1), new Point2d(5, 1), new Point2d(10, 6), new Point2d(20, 6));
1271
1272 PolyLine2d offsetLine = reference.offsetLine(-10);
1273
1274
1275 double slope = 0.25;
1276 double slopeAngle = Math.atan2(slope, 1);
1277 double prevProjection = -100;
1278 List<Point2d> projections = new ArrayList<>();
1279 for (double x = -0.5; x < 19; x += 0.25)
1280 {
1281 double y = -5 + x * slope;
1282 Ray2d ray = new Ray2d(x, y, slopeAngle);
1283 projections.add(ray);
1284 double projectionLocation = offsetLine.projectRay(ray);
1285 if (Double.isNaN(projectionLocation))
1286 {
1287 offsetLine.projectRay(ray);
1288 }
1289 assertFalse("There is a projection", Double.isNaN(projectionLocation));
1290 Point2d projectedPoint = offsetLine.getLocation(projectionLocation);
1291
1292
1293 projections.add(projectedPoint);
1294 projections.add(ray);
1295 assertTrue("projection increases monotonous", projectionLocation > prevProjection);
1296 prevProjection = projectionLocation;
1297 }
1298
1299 projections.clear();
1300 prevProjection = -100;
1301 for (double x = 1.5; x < 21; x += 0.25)
1302 {
1303 double y = -15 + x * slope;
1304 Ray2d ray = new Ray2d(x, y, slopeAngle);
1305 double projectionLocation = offsetLine.projectRay(ray);
1306 if (Double.isNaN(projectionLocation))
1307 {
1308 System.out.println("x " + x + " gives NaN result");
1309 continue;
1310 }
1311 projections.add(ray);
1312 Point2d projectedPoint = offsetLine.getLocation(projectionLocation);
1313
1314
1315 projections.add(projectedPoint);
1316 projections.add(ray);
1317 assertTrue("projection increases monotonous", projectionLocation > prevProjection);
1318 prevProjection = projectionLocation;
1319 }
1320
1321 }
1322
1323
1324
1325
1326 @Test
1327 public void testExports()
1328 {
1329 Point2d[] points =
1330 new Point2d[] { new Point2d(123.456, 345.678), new Point2d(234.567, 456.789), new Point2d(-12.345, -34.567) };
1331 PolyLine2d pl = new PolyLine2d(points);
1332 String[] out = pl.toExcel().split("\\n");
1333 assertEquals("Excel output consists of one line per point", points.length, out.length);
1334 for (int index = 0; index < points.length; index++)
1335 {
1336 String[] fields = out[index].split("\\t");
1337 assertEquals("each line consists of two fields", 2, fields.length);
1338 try
1339 {
1340 double x = Double.parseDouble(fields[0].trim());
1341 assertEquals("x matches", points[index].x, x, 0.001);
1342 }
1343 catch (NumberFormatException nfe)
1344 {
1345 fail("First field " + fields[0] + " does not parse as a double");
1346 }
1347 try
1348 {
1349 double y = Double.parseDouble(fields[1].trim());
1350 assertEquals("y matches", points[index].y, y, 0.001);
1351 }
1352 catch (NumberFormatException nfe)
1353 {
1354 fail("Second field " + fields[1] + " does not parse as a double");
1355 }
1356 }
1357
1358 out = pl.toPlot().split(" L");
1359 assertEquals("Plotter output consists of one coordinate pair per point", points.length, out.length);
1360 for (int index = 0; index < points.length; index++)
1361 {
1362 String[] fields = out[index].split(",");
1363 assertEquals("each line consists of two fields", 2, fields.length);
1364 if (index == 0)
1365 {
1366 assertTrue(fields[0].startsWith("M"));
1367 fields[0] = fields[0].substring(1);
1368 }
1369 try
1370 {
1371 double x = Double.parseDouble(fields[0].trim());
1372 assertEquals("x matches", points[index].x, x, 0.001);
1373 }
1374 catch (NumberFormatException nfe)
1375 {
1376 fail("First field " + fields[0] + " does not parse as a double");
1377 }
1378 try
1379 {
1380 double y = Double.parseDouble(fields[1].trim());
1381 assertEquals("y matches", points[index].y, y, 0.001);
1382 }
1383 catch (NumberFormatException nfe)
1384 {
1385 fail("Second field " + fields[1] + " does not parse as a double");
1386 }
1387 }
1388 }
1389
1390
1391
1392
1393
1394
1395
1396 private void verifyPointsAndSegments(final PolyLine2d line, final Point2d[] points) throws DrawRuntimeException
1397 {
1398 assertEquals("Line should have same number of points as point array", line.size(), points.length);
1399 for (int i = 0; i < points.length; i++)
1400 {
1401 assertEquals("x of point i should match", points[i].x, line.get(i).x, Math.ulp(points[i].x));
1402 assertEquals("y of point i should match", points[i].y, line.get(i).y, Math.ulp(points[i].y));
1403 assertEquals("x of point i should match", points[i].x, line.getX(i), Math.ulp(points[i].x));
1404 assertEquals("y of point i should match", points[i].y, line.getY(i), Math.ulp(points[i].y));
1405 if (i < points.length - 1)
1406 {
1407 LineSegment2d segment = line.getSegment(i);
1408 assertEquals("begin x of line segment i should match", points[i].x, segment.startX, Math.ulp(points[i].x));
1409 assertEquals("begin y of line segment i should match", points[i].y, segment.startY, Math.ulp(points[i].y));
1410 assertEquals("end x of line segment i should match", points[i + 1].x, segment.endX, Math.ulp(points[i + 1].x));
1411 assertEquals("end y of line segment i should match", points[i + 1].y, segment.endY, Math.ulp(points[i + 1].y));
1412 }
1413 else
1414 {
1415 try
1416 {
1417 line.getSegment(i);
1418 fail("Too large index should have thrown a DrawRuntimeException");
1419 }
1420 catch (DrawRuntimeException dre)
1421 {
1422
1423 }
1424
1425 try
1426 {
1427 line.getSegment(-1);
1428 fail("Negative index should have thrown a DrawRuntimeException");
1429 }
1430 catch (DrawRuntimeException dre)
1431 {
1432
1433 }
1434
1435 }
1436 }
1437 }
1438
1439
1440
1441
1442 @Test
1443 public void testTransitionLine()
1444 {
1445
1446 PolyLine2d bezier = Bezier.cubic(64, new Ray2d(-5, 0, 0, 0), new Ray2d(0, 5, 0, 7));
1447
1448 double length = bezier.getLength();
1449 double prevDir = Double.NaN;
1450 for (int step = 0; step <= 1000; step++)
1451 {
1452 double distance = length * step / 1000;
1453 Ray2d ray = bezier.getLocation(distance);
1454 double direction = Math.toDegrees(ray.phi);
1455 if (step > 0)
1456 {
1457 assertEquals("phi changes very little at step " + step, prevDir, direction, 2);
1458 }
1459 prevDir = Math.toDegrees(ray.phi);
1460 }
1461
1462 PolyLine2d transitioningOffsetLine = bezier.offsetLine(0, 2);
1463
1464 length = transitioningOffsetLine.getLength();
1465 prevDir = Double.NaN;
1466 for (int step = 0; step <= 1000; step++)
1467 {
1468 double distance = length * step / 1000;
1469 Ray2d ray = transitioningOffsetLine.getLocation(distance);
1470 double direction = Math.toDegrees(ray.phi);
1471 if (step > 0)
1472 {
1473 assertEquals("phi changes very little at step " + step, prevDir, direction, 2);
1474 }
1475 prevDir = Math.toDegrees(ray.phi);
1476 }
1477 PolyLine2d endLine = bezier.offsetLine(-2);
1478
1479 TransitionFunction transitionFunction = new TransitionFunction()
1480 {
1481 @Override
1482 public double function(final double fraction)
1483 {
1484 return 0.5 - Math.cos(fraction * Math.PI) / 2;
1485 }
1486 };
1487 PolyLine2d cosineSmoothTransitioningLine = bezier.transitionLine(endLine, transitionFunction);
1488
1489 length = cosineSmoothTransitioningLine.getLength();
1490 prevDir = Double.NaN;
1491 for (int step = 0; step <= 1000; step++)
1492 {
1493 double distance = length * step / 1000;
1494 Ray2d ray = cosineSmoothTransitioningLine.getLocation(distance);
1495 double direction = Math.toDegrees(ray.phi);
1496 if (step > 0)
1497 {
1498 assertEquals("phi changes very little at step " + step, prevDir, direction, 4);
1499 }
1500 prevDir = Math.toDegrees(ray.phi);
1501 }
1502
1503
1504
1505 PolyLine2d cosineSmoothTransitioningLine2 =
1506 endLine.reverse().transitionLine(bezier.reverse(), transitionFunction).reverse();
1507
1508 assertEquals("Lengths are equal", cosineSmoothTransitioningLine.getLength(), cosineSmoothTransitioningLine2.getLength(),
1509 0.001);
1510 for (int step = 0; step <= 1000; step++)
1511 {
1512 Ray2d ray1 = cosineSmoothTransitioningLine.getLocation(step * cosineSmoothTransitioningLine.getLength() / 1000);
1513 Ray2d ray2 = cosineSmoothTransitioningLine2.getLocation(step * cosineSmoothTransitioningLine2.getLength() / 1000);
1514 assertEquals("rays are almost equal in x", ray1.x, ray2.x, 0.001);
1515 assertEquals("rays are almost equal in y", ray1.y, ray2.y, 0.001);
1516 assertEquals("rays are almost equal in phi", ray1.phi, ray2.phi, 0.0001);
1517 }
1518
1519 assertEquals("offset by zero returns original", bezier, bezier.offsetLine(0, 0));
1520 assertEquals("offset by constant with two arguments returns same as offset with one argument", bezier.offsetLine(3, 3),
1521 bezier.offsetLine(3));
1522 }
1523
1524
1525
1526
1527
1528 @Test
1529 public final void filterTest() throws DrawRuntimeException
1530 {
1531 Point2d[] tooShort = new Point2d[] {};
1532 try
1533 {
1534 new PolyLine2d(true, tooShort);
1535 fail("Array with no points should have thrown an exception");
1536 }
1537 catch (DrawRuntimeException dre)
1538 {
1539
1540 }
1541
1542 tooShort = new Point2d[] { new Point2d(1, 2) };
1543 try
1544 {
1545 new PolyLine2d(true, tooShort);
1546 fail("Array with one point should have thrown an exception");
1547 }
1548 catch (DrawRuntimeException dre)
1549 {
1550
1551 }
1552
1553 Point2d p0 = new Point2d(1, 2);
1554 Point2d p1 = new Point2d(4, 5);
1555 Point2d[] points = new Point2d[] { p0, p1 };
1556 PolyLine2d result = new PolyLine2d(true, points);
1557 assertTrue("first point is p0", p0.equals(result.get(0)));
1558 assertTrue("second point is p1", p1.equals(result.get(1)));
1559 Point2d p1Same = new Point2d(4, 5);
1560 result = new PolyLine2d(true, new Point2d[] { p0, p0, p0, p0, p1Same, p0, p1, p1, p1Same, p1, p1 });
1561 assertEquals("result should contain 4 points", 4, result.size());
1562 assertTrue("first point is p0", p0.equals(result.get(0)));
1563 assertTrue("second point is p1", p1.equals(result.get(1)));
1564 assertTrue("third point is p0", p0.equals(result.get(0)));
1565 assertTrue("last point is p1", p1.equals(result.get(1)));
1566 new PolyLine2d(true, new Point2d[] { p0, new Point2d(1, 3) });
1567
1568 try
1569 {
1570 PolyLine2d.cleanPoints(true, null);
1571 fail("null iterator should have thrown a NullPointerException");
1572 }
1573 catch (NullPointerException npe)
1574 {
1575
1576 }
1577
1578 try
1579 {
1580 PolyLine2d.cleanPoints(true, new Iterator<Point2d>()
1581 {
1582 @Override
1583 public boolean hasNext()
1584 {
1585 return false;
1586 }
1587
1588 @Override
1589 public Point2d next()
1590 {
1591 return null;
1592 }
1593 });
1594 fail("Iterator that has no data should have thrown a DrawRuntimeException");
1595 }
1596 catch (DrawRuntimeException dre)
1597 {
1598
1599 }
1600
1601 Iterator<Point2d> iterator =
1602 PolyLine2d.cleanPoints(true, Arrays.stream(new Point2d[] { new Point2d(1, 2) }).iterator());
1603 iterator.next();
1604 assertFalse("iterator should now be out of data", iterator.hasNext());
1605 try
1606 {
1607 iterator.next();
1608 fail("Iterator that has no nore data should have thrown a NoSuchElementException");
1609 }
1610 catch (NoSuchElementException nse)
1611 {
1612
1613 }
1614
1615
1616 iterator = PolyLine2d.cleanPoints(false,
1617 Arrays.stream(new Point2d[] { new Point2d(1, 2), new Point2d(1, 2), new Point2d(1, 2) }).iterator());
1618 assertTrue("iterator has initial point", iterator.hasNext());
1619 iterator.next();
1620 assertTrue("iterator has second point", iterator.hasNext());
1621 iterator.next();
1622 assertTrue("iterator has second point", iterator.hasNext());
1623 iterator.next();
1624 assertFalse("iterator has no more data", iterator.hasNext());
1625 }
1626
1627
1628
1629
1630
1631
1632 @SuppressWarnings("unlikely-arg-type")
1633 @Test
1634 public void testToStringHashCodeAndEquals() throws NullPointerException, DrawRuntimeException
1635 {
1636 PolyLine2d line = new PolyLine2d(new Point2d[] { new Point2d(1, 2), new Point2d(4, 6), new Point2d(8, 9) });
1637 assertTrue("toString returns something descriptive", line.toString().startsWith("PolyLine2d ["));
1638 assertFalse("toString does not startHeading", line.toString().contains("startHeading"));
1639 assertTrue("toString can suppress the class name", line.toString().indexOf(line.toString(true)) > 0);
1640
1641
1642 assertNotEquals("hash code takes x coordinate into account",
1643 new PolyLine2d(new Point2d(0, 0), new Point2d(1, 1)).hashCode(),
1644 new PolyLine2d(new Point2d(1, 0), new Point2d(1, 1)).hashCode());
1645 assertNotEquals("hash code takes y coordinate into account",
1646 new PolyLine2d(new Point2d(0, 0), new Point2d(1, 1)).hashCode(),
1647 new PolyLine2d(new Point2d(0, 1), new Point2d(1, 1)).hashCode());
1648 assertNotEquals("hash code takes x coordinate into account",
1649 new PolyLine2d(new Point2d(0, 0), new Point2d(1, 1)).hashCode(),
1650 new PolyLine2d(new Point2d(0, 0), new Point2d(2, 1)).hashCode());
1651 assertNotEquals("hash code takes y coordinate into account",
1652 new PolyLine2d(new Point2d(0, 0), new Point2d(1, 1)).hashCode(),
1653 new PolyLine2d(new Point2d(0, 0), new Point2d(1, 2)).hashCode());
1654
1655
1656 assertTrue("line is equal to itself", line.equals(line));
1657 assertFalse("line is not equal to a different line",
1658 line.equals(new PolyLine3d(new Point3d(123, 456, 789), new Point3d(789, 101112, 2))));
1659 assertFalse("line is not equal to null", line.equals(null));
1660 assertFalse("line is not equal to a different kind of object", line.equals("unlikely"));
1661 assertEquals("equals verbatim copy", line,
1662 new PolyLine2d(new Point2d[] { new Point2d(1, 2), new Point2d(4, 6), new Point2d(8, 9) }));
1663 assertNotEquals("equals checks x", line,
1664 new PolyLine2d(new Point2d[] { new Point2d(2, 2), new Point2d(4, 6), new Point2d(8, 9) }));
1665 assertNotEquals("equals checks y", line,
1666 new PolyLine2d(new Point2d[] { new Point2d(1, 2), new Point2d(4, 7), new Point2d(8, 9) }));
1667 assertTrue("Line is equal to line from same set of points", line.equals(new PolyLine2d(line.getPoints())));
1668 }
1669
1670
1671
1672
1673 @Test
1674 public void testDegenerate()
1675 {
1676 try
1677 {
1678 new PolyLine2d(Double.NaN, 2, 3);
1679 fail("NaN should have thrown a DrawRuntimeException");
1680 }
1681 catch (DrawRuntimeException dre)
1682 {
1683
1684 }
1685
1686 try
1687 {
1688 new PolyLine2d(1, Double.NaN, 3);
1689 fail("NaN should have thrown a DrawRuntimeException");
1690 }
1691 catch (DrawRuntimeException dre)
1692 {
1693
1694 }
1695
1696 try
1697 {
1698 new PolyLine2d(1, 2, Double.NaN);
1699 fail("NaN should have thrown a DrawRuntimeException");
1700 }
1701 catch (DrawRuntimeException dre)
1702 {
1703
1704 }
1705
1706 try
1707 {
1708 new PolyLine2d(1, 2, Double.POSITIVE_INFINITY);
1709 fail("NaN should have thrown a DrawRuntimeException");
1710 }
1711 catch (DrawRuntimeException dre)
1712 {
1713
1714 }
1715
1716 try
1717 {
1718 new PolyLine2d(1, 2, Double.NEGATIVE_INFINITY);
1719 fail("NaN should have thrown a DrawRuntimeException");
1720 }
1721 catch (DrawRuntimeException dre)
1722 {
1723
1724 }
1725
1726 PolyLine2d l = new PolyLine2d(1, 2, 3);
1727 assertEquals("length is 0", 0, l.getLength(), 0);
1728 assertEquals("size is 1", 1, l.size());
1729 assertEquals("getX(0) is 1", 1, l.getX(0), 0);
1730 assertEquals("getY(0) is 2", 2, l.getY(0), 0);
1731 Ray2d r = l.getLocation(0.0);
1732 assertEquals("heading at 0", 3, r.getPhi(), 0);
1733 assertEquals("x at 0 is 1", 1, r.getX(), 0);
1734 assertEquals("y at 0 is 2", 2, r.getY(), 0);
1735 assertEquals("bounds", new Bounds2d(l.get(0)), l.getBounds());
1736 try
1737 {
1738 l.getLocation(0.1);
1739 fail("location at position != 0 should have thrown a DrawRuntimeException");
1740 }
1741 catch (DrawRuntimeException dre)
1742 {
1743
1744 }
1745
1746 try
1747 {
1748 l.getLocation(-0.1);
1749 fail("location at position != 0 should have thrown a DrawRuntimeException");
1750 }
1751 catch (DrawRuntimeException dre)
1752 {
1753
1754 }
1755
1756 try
1757 {
1758 new PolyLine2d(new Point2d(1, 2), Double.NaN);
1759 fail("NaN should have thrown a DrawRuntimeException");
1760 }
1761 catch (DrawRuntimeException dre)
1762 {
1763
1764 }
1765
1766 try
1767 {
1768 new PolyLine2d((Ray2d) null);
1769 fail("null pointer should have thrown a NullPointerException");
1770 }
1771 catch (NullPointerException npe)
1772 {
1773
1774 }
1775
1776 assertEquals("closest point is the point", r, l.closestPointOnPolyLine(new Point2d(4, -2)));
1777
1778 PolyLine2d straightX = new PolyLine2d(1, 2, 0);
1779 for (int x = -10; x <= 10; x += 1)
1780 {
1781 for (int y = -10; y <= 10; y += 1)
1782 {
1783 Point2d testPoint = new Point2d(x, y);
1784 assertEquals("closest point extended", r.projectOrthogonalExtended(testPoint),
1785 l.projectOrthogonalExtended(testPoint));
1786 assertEquals("closest point on degenerate line is the point of the degenerate line", l.getLocation(0.0),
1787 l.closestPointOnPolyLine(testPoint));
1788 if (x == 1)
1789 {
1790 assertEquals("projection on horizontal degenerate line hits", straightX.get(0),
1791 straightX.projectOrthogonal(testPoint));
1792 }
1793 else
1794 {
1795 assertNull("projection on horizontal degenerate line misses", straightX.projectOrthogonal(testPoint));
1796 }
1797 if (x == 1 && y == 2)
1798 {
1799 assertEquals("NonExtended projection will return point for exact match", testPoint,
1800 l.projectOrthogonal(testPoint));
1801 assertEquals("NonExtended fractional projection returns 0 for exact match", 0,
1802 l.projectOrthogonalFractional(testPoint), 0);
1803 assertEquals("Extended fractional projection returns 0 for exact match", 0,
1804 l.projectOrthogonalFractionalExtended(testPoint), 0);
1805 }
1806 else
1807 {
1808 assertNull("For non-nice directions nonExtended projection will return null if point does not match",
1809 l.projectOrthogonal(testPoint));
1810 assertTrue("For non-nice directions non-extended fractional projection will return NaN if point does "
1811 + "not match", Double.isNaN(l.projectOrthogonalFractional(testPoint)));
1812 if (l.getLocation(0.0).projectOrthogonalFractional(testPoint) > 0)
1813 {
1814 assertTrue(
1815 "ProjectOrthogonalFractionalExtended returns POSITIVE_INFINITY of projection misses "
1816 + "along startHeading side",
1817 Double.POSITIVE_INFINITY == l.projectOrthogonalFractionalExtended(testPoint));
1818 }
1819 else
1820 {
1821 assertTrue(
1822 "ProjectOrthogonalFractionalExtended returns POSITIVE_INFINITY of projection misses "
1823 + ", but not along startHeading side",
1824 Double.NEGATIVE_INFINITY == l.projectOrthogonalFractionalExtended(testPoint));
1825 }
1826 }
1827 if (x == 1)
1828 {
1829 assertEquals("Non-Extended projection will return point for matching X for line along X", straightX.get(0),
1830 straightX.projectOrthogonal(testPoint));
1831 }
1832 else
1833 {
1834 assertNull("Non-Extended projection will return null for non matching X for line along X",
1835 straightX.projectOrthogonal(testPoint));
1836 }
1837 }
1838 }
1839
1840 l = new PolyLine2d(new Point2d(1, 2), 3);
1841 assertEquals("length is 0", 0, l.getLength(), 0);
1842 assertEquals("size is 1", 1, l.size());
1843 assertEquals("getX(0) is 1", 1, l.getX(0), 0);
1844 assertEquals("getY(0) is 2", 2, l.getY(0), 0);
1845 r = l.getLocation(0.0);
1846 assertEquals("heading at 0", 3, r.getPhi(), 0);
1847 assertEquals("x at 0 is 1", 1, r.getX(), 0);
1848 assertEquals("y at 0 is 2", 2, r.getY(), 0);
1849
1850 l = new PolyLine2d(new Ray2d(1, 2, 3));
1851 assertEquals("length is 0", 0, l.getLength(), 0);
1852 assertEquals("size is 1", 1, l.size());
1853 assertEquals("getX(0) is 1", 1, l.getX(0), 0);
1854 assertEquals("getY(0) is 2", 2, l.getY(0), 0);
1855 r = l.getLocation(0.0);
1856 assertEquals("heading at 0", 3, r.getPhi(), 0);
1857 assertEquals("x at 0 is 1", 1, r.getX(), 0);
1858 assertEquals("y at 0 is 2", 2, r.getY(), 0);
1859
1860 PolyLine2d notEqual = new PolyLine2d(1, 2, 4);
1861 assertNotEquals("Check that the equals method verifies the startHeading", l, notEqual);
1862
1863 assertTrue("toString contains startHeading", l.toString().contains("startHeading"));
1864 }
1865
1866
1867
1868
1869
1870 @Test
1871 public void testOTS2Problem() throws DrawRuntimeException
1872 {
1873 PolyLine2d line = new PolyLine2d(new Point2d(100, 0), new Point2d(100.1, 0));
1874 double length = line.getLength();
1875 line.getLocation(length - Math.ulp(length));
1876 }
1877
1878 }