1 package org.djutils.draw.line;
2
3 import java.awt.geom.Path2D;
4 import java.awt.geom.PathIterator;
5 import java.util.ArrayList;
6 import java.util.Arrays;
7 import java.util.Iterator;
8 import java.util.List;
9 import java.util.Locale;
10 import java.util.NoSuchElementException;
11 import java.util.function.Function;
12
13 import org.djutils.draw.DrawRuntimeException;
14 import org.djutils.draw.Drawable2d;
15 import org.djutils.draw.bounds.Bounds2d;
16 import org.djutils.draw.point.DirectedPoint2d;
17 import org.djutils.draw.point.Point2d;
18 import org.djutils.exceptions.Throw;
19 import org.djutils.logger.CategoryLogger;
20
21
22
23
24
25
26
27
28
29
30 public class PolyLine2d implements Drawable2d, PolyLine<PolyLine2d, Point2d, Ray2d, DirectedPoint2d, LineSegment2d>
31 {
32
33 private static final long serialVersionUID = 20200911L;
34
35
36 private final double[] x;
37
38
39 private final double[] y;
40
41
42 private final double[] lengthIndexedLine;
43
44
45 private final double length;
46
47
48 private final Bounds2d bounds;
49
50
51 private final double startHeading;
52
53
54
55
56
57
58
59
60
61
62 PolyLine2d(final boolean copyNeeded, final double[] x, final double[] y) throws NullPointerException, DrawRuntimeException
63 {
64 Throw.whenNull(x, "x");
65 Throw.whenNull(y, "y");
66 Throw.when(x.length != y.length, DrawRuntimeException.class, "x and y arrays must have same length");
67 Throw.when(x.length < 2, DrawRuntimeException.class, "Need at least two points");
68 this.x = copyNeeded ? Arrays.copyOf(x, x.length) : x;
69 this.y = copyNeeded ? Arrays.copyOf(y, y.length) : y;
70 double minX = x[0];
71 double minY = y[0];
72 double maxX = x[0];
73 double maxY = y[0];
74 this.lengthIndexedLine = new double[x.length];
75 this.lengthIndexedLine[0] = 0.0;
76 for (int i = 1; i < x.length; i++)
77 {
78 minX = Math.min(minX, x[i]);
79 minY = Math.min(minY, y[i]);
80 maxX = Math.max(maxX, x[i]);
81 maxY = Math.max(maxY, y[i]);
82 if (x[i - 1] == x[i] && y[i - 1] == y[i])
83 {
84 throw new DrawRuntimeException(
85 "Degenerate PolyLine2d; point " + (i - 1) + " has the same x and y as point " + i);
86 }
87 this.lengthIndexedLine[i] = this.lengthIndexedLine[i - 1] + Math.hypot(x[i] - x[i - 1], y[i] - y[i - 1]);
88 }
89 this.length = this.lengthIndexedLine[this.lengthIndexedLine.length - 1];
90 this.bounds = new Bounds2d(minX, maxX, minY, maxY);
91 this.startHeading = Double.NaN;
92 }
93
94
95
96
97
98
99
100
101 public PolyLine2d(final double x, final double y, final double heading) throws DrawRuntimeException
102 {
103 Throw.when(Double.isNaN(x), DrawRuntimeException.class, "x may not be NaN");
104 Throw.when(Double.isNaN(y), DrawRuntimeException.class, "y may not be NaN");
105 Throw.when(Double.isNaN(heading), DrawRuntimeException.class, "heading may not be NaN");
106 Throw.when(Double.isInfinite(heading), DrawRuntimeException.class, "heading must be finite");
107 this.x = new double[] {x};
108 this.y = new double[] {y};
109 this.startHeading = heading;
110 this.length = 0;
111 this.bounds = new Bounds2d(x, x, y, y);
112 this.lengthIndexedLine = new double[] {0.0};
113 }
114
115
116
117
118
119
120
121
122 public PolyLine2d(final Point2d p, final double heading) throws NullPointerException, DrawRuntimeException
123 {
124 this(Throw.whenNull(p, "p").x, p.y, heading);
125 }
126
127
128
129
130
131
132
133 public PolyLine2d(final DirectedPoint2d directedPoint2d) throws NullPointerException, DrawRuntimeException
134 {
135 this(Throw.whenNull(directedPoint2d, "r").x, directedPoint2d.y, directedPoint2d.dirZ);
136 }
137
138
139
140
141
142
143
144
145
146
147 public PolyLine2d(final double[] x, final double[] y) throws NullPointerException, DrawRuntimeException
148 {
149 this(true, x, y);
150 }
151
152
153
154
155
156
157
158
159 public PolyLine2d(final Point2d[] points) throws NullPointerException, DrawRuntimeException
160 {
161 this(false, makeArray(Throw.whenNull(points, "points"), p -> p.x), makeArray(points, p -> p.y));
162 }
163
164
165
166
167
168
169
170 protected static double[] makeArray(final Point2d[] points, final Function<Point2d, Double> getter)
171 {
172 double[] array = new double[points.length];
173 for (int index = 0; index < points.length; index++)
174 {
175 array[index] = getter.apply(points[index]);
176 }
177 return array;
178 }
179
180
181
182
183
184
185
186
187
188
189 public PolyLine2d(final Point2d point1, final Point2d point2, final Point2d... otherPoints)
190 throws NullPointerException, DrawRuntimeException
191 {
192 this(spliceArray(Throw.whenNull(point1, "point1"), Throw.whenNull(point2, "point2"), otherPoints));
193 }
194
195
196
197
198
199
200
201
202
203 private static Point2d[] spliceArray(final Point2d point1, final Point2d point2, final Point2d... otherPoints)
204 {
205 Point2d[] result = new Point2d[2 + (otherPoints == null ? 0 : otherPoints.length)];
206 result[0] = point1;
207 result[1] = point2;
208 if (otherPoints != null)
209 {
210 for (int i = 0; i < otherPoints.length; i++)
211 {
212 result[i + 2] = otherPoints[i];
213 }
214 }
215 return result;
216 }
217
218
219
220
221
222
223
224 public PolyLine2d(final Iterator<Point2d> iterator) throws NullPointerException, DrawRuntimeException
225 {
226 this(iteratorToList(Throw.whenNull(iterator, "iterator")));
227 }
228
229
230
231
232
233
234
235
236 public PolyLine2d(final List<Point2d> pointList) throws DrawRuntimeException, NullPointerException
237 {
238 this(pointList.toArray(new Point2d[Throw.whenNull(pointList, "pointList").size()]));
239 }
240
241
242
243
244
245
246
247
248 public PolyLine2d(final Path2D path) throws DrawRuntimeException, NullPointerException
249 {
250 this(path2DtoArray(Throw.whenNull(path, "path")));
251 }
252
253
254
255
256
257
258
259 private static Point2d[] path2DtoArray(final Path2D path) throws DrawRuntimeException
260 {
261 List<Point2d> result = new ArrayList<>();
262 for (PathIterator pi = path.getPathIterator(null); !pi.isDone(); pi.next())
263 {
264 double[] p = new double[6];
265 int segType = pi.currentSegment(p);
266 if (segType == PathIterator.SEG_MOVETO || segType == PathIterator.SEG_LINETO)
267 {
268 result.add(new Point2d(p[0], p[1]));
269 }
270 else if (segType == PathIterator.SEG_CLOSE)
271 {
272 if (!result.get(0).equals(result.get(result.size() - 1)))
273 {
274 result.add(result.get(0));
275 }
276 break;
277 }
278 else
279 {
280 throw new DrawRuntimeException("path2DtoArray only handles SEG_MOVETO, SEG_LINETO and SEG_CLOSE");
281 }
282 }
283 return result.toArray(new Point2d[result.size() - 1]);
284 }
285
286
287
288
289
290
291 protected static List<Point2d> iteratorToList(final Iterator<Point2d> iterator)
292 {
293 List<Point2d> result = new ArrayList<>();
294 iterator.forEachRemaining(result::add);
295 return result;
296 }
297
298
299
300
301
302
303
304 public PolyLine2d(final boolean filterDuplicates, final Point2d... points) throws DrawRuntimeException
305 {
306 this(PolyLine2d.cleanPoints(filterDuplicates, Arrays.stream(points).iterator()));
307 }
308
309
310
311
312
313
314
315
316 public PolyLine2d(final boolean filterDuplicates, final List<Point2d> pointList) throws DrawRuntimeException
317 {
318 this(PolyLine2d.cleanPoints(filterDuplicates, pointList.iterator()));
319 }
320
321
322
323
324
325
326
327 static Iterator<Point2d> cleanPoints(final boolean filter, final Iterator<Point2d> iterator)
328 {
329 Throw.whenNull(iterator, "Iterator");
330 Throw.when(!iterator.hasNext(), DrawRuntimeException.class, "Iterator has no points to return");
331 if (!filter)
332 {
333 return iterator;
334 }
335 return new Iterator<Point2d>()
336 {
337 private Point2d currentPoint = iterator.next();
338
339 @Override
340 public boolean hasNext()
341 {
342 return this.currentPoint != null;
343 }
344
345 @Override
346 public Point2d next()
347 {
348 Throw.when(this.currentPoint == null, NoSuchElementException.class, "Out of input");
349 Point2d result = this.currentPoint;
350 this.currentPoint = null;
351 while (iterator.hasNext())
352 {
353 this.currentPoint = iterator.next();
354 if (result.x != this.currentPoint.x || result.y != this.currentPoint.y)
355 {
356 break;
357 }
358 this.currentPoint = null;
359 }
360 return result;
361 }
362 };
363 }
364
365
366
367
368
369 public PolyLine2d(final PolyLine2d polyLine)
370 {
371 this.x = polyLine.x;
372 this.y = polyLine.y;
373 this.lengthIndexedLine = polyLine.lengthIndexedLine;
374 this.length = polyLine.length;
375 this.bounds = polyLine.bounds;
376 this.startHeading = polyLine.startHeading;
377 }
378
379 @Override
380 public PolyLine2d instantiate(final List<Point2d> pointList) throws NullPointerException, DrawRuntimeException
381 {
382 return new PolyLine2d(pointList);
383 }
384
385 @Override
386 public int size()
387 {
388 return this.x.length;
389 }
390
391 @Override
392 public final Point2d get(final int i) throws IndexOutOfBoundsException
393 {
394 return new Point2d(this.x[i], this.y[i]);
395 }
396
397 @Override
398 public final double getX(final int i) throws IndexOutOfBoundsException
399 {
400 return this.x[i];
401 }
402
403 @Override
404 public final double getY(final int i) throws IndexOutOfBoundsException
405 {
406 return this.y[i];
407 }
408
409 @Override
410 public LineSegment2d getSegment(final int index)
411 {
412 Throw.when(index < 0 || index >= this.x.length - 1, DrawRuntimeException.class, "index must be in range 0..size() - 1");
413 return new LineSegment2d(this.x[index], this.y[index], this.x[index + 1], this.y[index + 1]);
414 }
415
416 @Override
417 public final double lengthAtIndex(final int index)
418 {
419 return this.lengthIndexedLine[index];
420 }
421
422 @Override
423 public double getLength()
424 {
425 return this.length;
426 }
427
428 @Override
429 public Iterator<Point2d> getPoints()
430 {
431 return new Iterator<Point2d>()
432 {
433 private int nextIndex = 0;
434
435 @Override
436 public boolean hasNext()
437 {
438 return this.nextIndex < size();
439 }
440
441 @Override
442 public Point2d next()
443 {
444 return get(this.nextIndex++);
445 }
446 };
447 }
448
449 @Override
450 public Bounds2d getBounds()
451 {
452 return this.bounds;
453 }
454
455 @Override
456 public final PolyLine2d noiseFilteredLine(final double noiseLevel)
457 {
458 if (this.size() <= 2)
459 {
460 return this;
461 }
462 Point2d prevPoint = null;
463 List<Point2d> list = new ArrayList<>();
464 for (int index = 0; index < this.size(); index++)
465 {
466 Point2d currentPoint = get(index);
467 if (null != prevPoint && prevPoint.distance(currentPoint) < noiseLevel)
468 {
469 if (index == this.size() - 1)
470 {
471 if (list.size() > 1)
472 {
473
474 list.set(list.size() - 1, currentPoint);
475 }
476 else
477 {
478
479
480 list.add(currentPoint);
481 }
482 }
483 continue;
484 }
485 list.add(currentPoint);
486 prevPoint = currentPoint;
487 }
488 if (list.size() == this.x.length)
489 {
490 return this;
491 }
492 if (list.size() == 2 && list.get(0).equals(list.get(1)))
493 {
494
495 list.add(1, get(1));
496 }
497 return new PolyLine2d(list);
498 }
499
500
501
502
503
504
505
506
507 public static PolyLine2d concatenate(final PolyLine2d... lines) throws DrawRuntimeException
508 {
509 return concatenate(0.0, lines);
510 }
511
512
513
514
515
516
517
518
519
520 public static PolyLine2d concatenate(final double tolerance, final PolyLine2d line1, final PolyLine2d line2)
521 throws DrawRuntimeException
522 {
523 if (line1.getLast().distance(line2.getFirst()) > tolerance)
524 {
525 throw new DrawRuntimeException("Lines are not connected: " + line1.getLast() + " to " + line2.getFirst()
526 + " distance is " + line1.getLast().distance(line2.getFirst()) + " > " + tolerance);
527 }
528 int size = line1.size() + line2.size() - 1;
529 Point2d[] points = new Point2d[size];
530 int nextIndex = 0;
531 for (int j = 0; j < line1.size(); j++)
532 {
533 points[nextIndex++] = line1.get(j);
534 }
535 for (int j = 1; j < line2.size(); j++)
536 {
537 points[nextIndex++] = line2.get(j);
538 }
539 return new PolyLine2d(points);
540 }
541
542
543
544
545
546
547
548
549
550
551 public static PolyLine2d concatenate(final double tolerance, final PolyLine2d... lines) throws DrawRuntimeException
552 {
553 if (0 == lines.length)
554 {
555 throw new DrawRuntimeException("Empty argument list");
556 }
557 else if (1 == lines.length)
558 {
559 return lines[0];
560 }
561 int size = lines[0].size();
562 for (int i = 1; i < lines.length; i++)
563 {
564 if (lines[i - 1].getLast().distance(lines[i].getFirst()) > tolerance)
565 {
566 throw new DrawRuntimeException(
567 "Lines are not connected: " + lines[i - 1].getLast() + " to " + lines[i].getFirst() + " distance is "
568 + lines[i - 1].getLast().distance(lines[i].getFirst()) + " > " + tolerance);
569 }
570 size += lines[i].size() - 1;
571 }
572 Point2d[] points = new Point2d[size];
573 int nextIndex = 0;
574 for (int i = 0; i < lines.length; i++)
575 {
576 PolyLine2d line = lines[i];
577 for (int j = 0 == i ? 0 : 1; j < line.size(); j++)
578 {
579 points[nextIndex++] = line.get(j);
580 }
581 }
582 return new PolyLine2d(points);
583 }
584
585 @Override
586 public final DirectedPoint2d getLocationExtended(final double position)
587 {
588 if (position >= 0.0 && position <= this.length)
589 {
590 return getLocation(position);
591 }
592
593
594 if (position < 0.0)
595 {
596 double fraction = position / (this.lengthIndexedLine[1] - this.lengthIndexedLine[0]);
597 return new DirectedPoint2d(this.x[0] + fraction * (this.x[1] - this.x[0]), this.y[0] + fraction * (this.y[1] - this.y[0]),
598 this.x[1], this.y[1]);
599 }
600
601
602
603 int n1 = this.x.length - 1;
604 int n2 = this.x.length - 2;
605 double len = position - this.length;
606 double fraction = len / (this.lengthIndexedLine[n1] - this.lengthIndexedLine[n2]);
607 while (Double.isInfinite(fraction))
608 {
609
610 if (--n2 < 0)
611 {
612 CategoryLogger.always().error("lengthIndexedLine of {} is invalid", this);
613 return new DirectedPoint2d(this.x[n1], this.y[n1], 0.0);
614 }
615 fraction = len / (this.lengthIndexedLine[n1] - this.lengthIndexedLine[n2]);
616 }
617 return new DirectedPoint2d(this.x[n1] + fraction * (this.x[n1] - this.x[n2]), this.y[n1] + fraction * (this.y[n1] - this.y[n2]),
618 Math.atan2(this.y[n1] - this.y[n2], this.x[n1] - this.x[n2]));
619 }
620
621 @Override
622 public final DirectedPoint2d getLocation(final double position) throws DrawRuntimeException
623 {
624 Throw.when(Double.isNaN(position), DrawRuntimeException.class, "position may not be NaN");
625 Throw.when(position < 0.0 || position > this.length, DrawRuntimeException.class,
626 "getLocation for line: position < 0.0 or > line length. Position = " + position + "; length = " + this.length);
627
628 if (position == 0.0)
629 {
630 if (this.lengthIndexedLine.length == 1)
631 {
632 return new DirectedPoint2d(this.x[0], this.y[0], this.startHeading);
633 }
634 return new DirectedPoint2d(this.x[0], this.y[0], this.x[1], this.y[1]);
635 }
636 if (position == this.length)
637 {
638 return new DirectedPoint2d(this.x[this.x.length - 1], this.y[this.x.length - 1],
639 2 * this.x[this.x.length - 1] - this.x[this.x.length - 2],
640 2 * this.y[this.x.length - 1] - this.y[this.x.length - 2]);
641 }
642
643 int index = find(position);
644 double remainder = position - this.lengthIndexedLine[index];
645 double fraction = remainder / (this.lengthIndexedLine[index + 1] - this.lengthIndexedLine[index]);
646
647
648
649
650
651
652
653 return new DirectedPoint2d(this.x[index] + fraction * (this.x[index + 1] - this.x[index]),
654 this.y[index] + fraction * (this.y[index + 1] - this.y[index]), 2 * this.x[index + 1] - this.x[index],
655 2 * this.y[index + 1] - this.y[index]);
656 }
657
658
659
660
661
662
663
664
665
666 private double projectOrthogonalFractional(final Point2d point, final Boolean limitHandling)
667 {
668 Throw.whenNull(point, "point");
669 double result = Double.NaN;
670 if (this.lengthIndexedLine.length == 1)
671 {
672
673 if (null != limitHandling && limitHandling)
674 {
675 return 0.0;
676 }
677 result = new Ray2d(getLocation(0.0)).projectOrthogonalFractionalExtended(point);
678 if (null == limitHandling)
679 {
680 return result == 0.0 ? 0.0 : Double.NaN;
681 }
682
683 if (result == 0.0)
684 {
685 return 0.0;
686 }
687 return result > 0 ? Double.POSITIVE_INFINITY : Double.NEGATIVE_INFINITY;
688 }
689 double bestDistance = Double.POSITIVE_INFINITY;
690 double bestDistanceExtended = Double.POSITIVE_INFINITY;
691 for (int index = 1; index < this.size(); index++)
692 {
693 double fraction = point.fractionalPositionOnLine(this.x[index - 1], this.y[index - 1], this.x[index], this.y[index],
694 false, false);
695 double distance = Math.hypot(point.x - (this.x[index - 1] + fraction * (this.x[index] - this.x[index - 1])),
696 point.y - (this.y[index - 1] + fraction * (this.y[index] - this.y[index - 1])));
697 if (distance < bestDistanceExtended && (fraction >= 0.0 && fraction <= 1.0 || (fraction < 0.0 && index == 1)
698 || fraction > 1.0 && index == this.size() - 1))
699 {
700 bestDistanceExtended = distance;
701 }
702 if (distance < bestDistance && (fraction >= 0.0 || index == 1 && limitHandling != null && !limitHandling)
703 && (fraction <= 1.0 || index == this.size() - 1 && limitHandling != null && !limitHandling))
704 {
705 bestDistance = distance;
706 result = lengthAtIndex(index - 1) + fraction * (lengthAtIndex(index) - lengthAtIndex(index - 1));
707 }
708 else if (fraction < 0.0 && limitHandling != null && limitHandling)
709 {
710 distance = Math.hypot(point.x - this.x[index - 1], point.y - this.y[index - 1]);
711 if (distance < bestDistance)
712 {
713 bestDistance = distance;
714 result = lengthAtIndex(index - 1);
715 }
716 }
717 else if (index == this.size() - 1 && limitHandling != null && limitHandling)
718 {
719 distance = Math.hypot(point.x - this.x[index], point.y - this.y[index]);
720 if (distance < bestDistance)
721 {
722 bestDistance = distance;
723 result = lengthAtIndex(index);
724 }
725 }
726 }
727 if (bestDistance > bestDistanceExtended && (limitHandling == null || !limitHandling))
728 {
729 return Double.NaN;
730 }
731 return result / this.length;
732 }
733
734 @Override
735 public Point2d closestPointOnPolyLine(final Point2d point)
736 {
737 return getLocation(projectOrthogonalFractional(point, true) * this.length);
738 }
739
740
741
742
743
744
745
746
747
748 private Point2d projectOrthogonal(final Point2d point, final Boolean limitHandling)
749 {
750 Throw.whenNull(point, "point");
751 if (this.lengthIndexedLine.length == 1)
752 {
753
754 Point2d result = new Ray2d(this.getLocation(0.0)).projectOrthogonalExtended(point);
755 if (null == limitHandling)
756 {
757 return result.x != this.x[0] || result.y != this.y[0] ? null : get(0);
758 }
759
760 return result;
761 }
762 double fraction = projectOrthogonalFractional(point, limitHandling);
763 if (Double.isNaN(fraction))
764 {
765 return null;
766 }
767 return getLocationExtended(fraction * this.length);
768 }
769
770 @Override
771 public Point2d projectOrthogonal(final Point2d point) throws NullPointerException
772 {
773 return projectOrthogonal(point, null);
774 }
775
776 @Override
777 public Point2d projectOrthogonalExtended(final Point2d point) throws NullPointerException
778 {
779 return projectOrthogonal(point, false);
780 }
781
782 @Override
783 public final double projectOrthogonalFractional(final Point2d point) throws NullPointerException
784 {
785 return projectOrthogonalFractional(point, null);
786 }
787
788 @Override
789 public double projectOrthogonalFractionalExtended(final Point2d point) throws NullPointerException
790 {
791 return projectOrthogonalFractional(point, false);
792 }
793
794 @Override
795 public PolyLine2d extract(final double start, final double end) throws DrawRuntimeException
796 {
797 if (Double.isNaN(start) || Double.isNaN(end) || start < 0 || start >= end || end > this.length)
798 {
799 throw new DrawRuntimeException(
800 "Bad interval (" + start + ".." + end + "; length of this PolyLine2d is " + this.length + ")");
801 }
802 double cumulativeLength = 0;
803 double nextCumulativeLength = 0;
804 double segmentLength = 0;
805 int index = 0;
806 List<Point2d> pointList = new ArrayList<>();
807 while (start > cumulativeLength)
808 {
809 Point2d fromPoint = get(index);
810 index++;
811 Point2d toPoint = get(index);
812 segmentLength = fromPoint.distance(toPoint);
813 cumulativeLength = nextCumulativeLength;
814 nextCumulativeLength = cumulativeLength + segmentLength;
815 if (nextCumulativeLength >= start)
816 {
817 break;
818 }
819 }
820 if (start == nextCumulativeLength)
821 {
822 pointList.add(get(index));
823 }
824 else
825 {
826 pointList.add(get(index - 1).interpolate(get(index), (start - cumulativeLength) / segmentLength));
827 if (end > nextCumulativeLength)
828 {
829 pointList.add(get(index));
830 }
831 }
832 while (end > nextCumulativeLength)
833 {
834 Point2d fromPoint = get(index);
835 index++;
836 if (index >= size())
837 {
838 break;
839 }
840 Point2d toPoint = get(index);
841 segmentLength = fromPoint.distance(toPoint);
842 cumulativeLength = nextCumulativeLength;
843 nextCumulativeLength = cumulativeLength + segmentLength;
844 if (nextCumulativeLength >= end)
845 {
846 break;
847 }
848 pointList.add(toPoint);
849 }
850 if (end == nextCumulativeLength)
851 {
852 pointList.add(get(index));
853 }
854 else if (index < this.x.length)
855 {
856 Point2d point = get(index - 1).interpolate(get(index), (end - cumulativeLength) / segmentLength);
857
858 if (!point.equals(pointList.get(pointList.size() - 1)))
859 {
860 pointList.add(point);
861 }
862 }
863
864 return instantiate(pointList);
865 }
866
867 @Override
868 public PolyLine2d truncate(final double position) throws DrawRuntimeException
869 {
870 if (position <= 0.0 || position > this.length)
871 {
872 throw new DrawRuntimeException("truncate for line: position <= 0.0 or > line length. Position = " + position
873 + ". Length = " + this.length + " m.");
874 }
875
876
877 if (position == this.length)
878 {
879 return this;
880 }
881
882
883 int index = find(position);
884 double remainder = position - lengthAtIndex(index);
885 double fraction = remainder / (lengthAtIndex(index + 1) - lengthAtIndex(index));
886 Point2d p1 = get(index);
887 Point2d lastPoint;
888 if (0.0 == fraction)
889 {
890 lastPoint = p1;
891 }
892 else
893 {
894 Point2d p2 = get(index + 1);
895 lastPoint = p1.interpolate(p2, fraction);
896 index++;
897 }
898 double[] truncatedX = new double[index + 1];
899 double[] truncatedY = new double[index + 1];
900 for (int i = 0; i < index; i++)
901 {
902 truncatedX[i] = this.x[i];
903 truncatedY[i] = this.y[i];
904 }
905 truncatedX[index] = lastPoint.x;
906 truncatedY[index] = lastPoint.y;
907 return new PolyLine2d(truncatedX, truncatedY);
908 }
909
910 @Override
911 @SuppressWarnings("checkstyle:methodlength")
912 public PolyLine2d offsetLine(final double offset, final double circlePrecision, final double offsetMinimumFilterValue,
913 final double offsetMaximumFilterValue, final double offsetFilterRatio, final double minimumOffset)
914 throws IllegalArgumentException
915 {
916 Throw.when(Double.isNaN(offset), IllegalArgumentException.class, "Offset may not be NaN");
917 Throw.when(Double.isNaN(circlePrecision) || circlePrecision <= 0, IllegalArgumentException.class,
918 "bad circlePrecision");
919 Throw.when(Double.isNaN(offsetMinimumFilterValue) || offsetMinimumFilterValue <= 0, IllegalArgumentException.class,
920 "bad offsetMinimumFilterValue");
921 Throw.when(Double.isNaN(offsetMaximumFilterValue) || offsetMaximumFilterValue <= 0, IllegalArgumentException.class,
922 "bad offsetMaximumFilterValue");
923 Throw.when(Double.isNaN(offsetFilterRatio) || offsetFilterRatio <= 0, IllegalArgumentException.class,
924 "bad offsetFilterRatio");
925 Throw.when(Double.isNaN(minimumOffset) || minimumOffset <= 0, IllegalArgumentException.class, "bad minimumOffset");
926 Throw.when(offsetMinimumFilterValue >= offsetMaximumFilterValue, IllegalArgumentException.class,
927 "bad offset filter values; minimum must be less than maximum");
928 double bufferOffset = Math.abs(offset);
929 if (bufferOffset < minimumOffset)
930 {
931 return this;
932 }
933
934 PolyLine2d filteredReferenceLine = noiseFilteredLine(
935 Math.max(offsetMinimumFilterValue, Math.min(bufferOffset / offsetFilterRatio, offsetMaximumFilterValue)));
936 List<Point2d> tempPoints = new ArrayList<>();
937
938 Point2d prevPoint = filteredReferenceLine.get(0);
939 Double prevAngle = null;
940 for (int index = 0; index < filteredReferenceLine.size() - 1; index++)
941 {
942 Point2d nextPoint = filteredReferenceLine.get(index + 1);
943 double angle = Math.atan2(nextPoint.y - prevPoint.y, nextPoint.x - prevPoint.x);
944 Point2d segmentFrom = new Point2d(prevPoint.x - Math.sin(angle) * offset, prevPoint.y + Math.cos(angle) * offset);
945 Point2d segmentTo = new Point2d(nextPoint.x - Math.sin(angle) * offset, nextPoint.y + Math.cos(angle) * offset);
946 boolean addSegment = true;
947 if (index > 0)
948 {
949 double deltaAngle = angle - prevAngle;
950 if (Math.abs(deltaAngle) > Math.PI)
951 {
952 deltaAngle -= Math.signum(deltaAngle) * 2 * Math.PI;
953 }
954 if (deltaAngle * offset <= 0)
955 {
956
957
958
959 int numSegments = 1;
960 if (Math.abs(deltaAngle) > Math.PI / 2)
961 {
962 numSegments = 2;
963 }
964 while (true)
965 {
966 double maxError = bufferOffset * (1 - Math.abs(Math.cos(deltaAngle / numSegments / 2)));
967 if (maxError < circlePrecision)
968 {
969 break;
970 }
971 numSegments *= 2;
972 }
973 Point2d prevArcPoint = tempPoints.get(tempPoints.size() - 1);
974
975 for (int additionalPoint = 1; additionalPoint < numSegments; additionalPoint++)
976 {
977 double intermediateAngle =
978 (additionalPoint * angle + (numSegments - additionalPoint) * prevAngle) / numSegments;
979 if (prevAngle * angle < 0 && Math.abs(prevAngle) > Math.PI / 2 && Math.abs(angle) > Math.PI / 2)
980 {
981 intermediateAngle += Math.PI;
982 }
983 Point2d intermediatePoint = new Point2d(prevPoint.x - Math.sin(intermediateAngle) * offset,
984 prevPoint.y + Math.cos(intermediateAngle) * offset);
985
986 Point2d prevSegFrom = null;
987 int stopAt = tempPoints.size();
988 for (int i = 0; i < stopAt; i++)
989 {
990 Point2d prevSegTo = tempPoints.get(i);
991 if (null != prevSegFrom)
992 {
993 Point2d prevSegIntersection = Point2d.intersectionOfLineSegments(prevArcPoint,
994 intermediatePoint, prevSegFrom, prevSegTo);
995 if (null != prevSegIntersection && prevSegIntersection.distance(prevArcPoint) > circlePrecision
996 && prevSegIntersection.distance(prevSegFrom) > circlePrecision
997 && prevSegIntersection.distance(prevSegTo) > circlePrecision)
998 {
999 tempPoints.add(prevSegIntersection);
1000
1001 }
1002 }
1003 prevSegFrom = prevSegTo;
1004 }
1005 Point2d nextSegmentIntersection =
1006 Point2d.intersectionOfLineSegments(prevSegFrom, intermediatePoint, segmentFrom, segmentTo);
1007 if (null != nextSegmentIntersection)
1008 {
1009 tempPoints.add(nextSegmentIntersection);
1010
1011 }
1012 tempPoints.add(intermediatePoint);
1013
1014 prevArcPoint = intermediatePoint;
1015 }
1016 }
1017
1018
1019 Point2d pPoint = null;
1020 int currentSize = tempPoints.size();
1021 for (int i = 0; i < currentSize ; i++)
1022 {
1023 Point2d p = tempPoints.get(i);
1024 if (null != pPoint)
1025 {
1026 double pAngle = Math.atan2(p.y - pPoint.y, p.x - pPoint.x);
1027 double angleDifference = angle - pAngle;
1028 if (Math.abs(angleDifference) > Math.PI)
1029 {
1030 angleDifference -= Math.signum(angleDifference) * 2 * Math.PI;
1031 }
1032 if (Math.abs(angleDifference) > 0)
1033 {
1034 Point2d intersection = Point2d.intersectionOfLineSegments(pPoint, p, segmentFrom, segmentTo);
1035 if (null != intersection)
1036 {
1037 if (tempPoints.size() - 1 == i)
1038 {
1039 tempPoints.remove(tempPoints.size() - 1);
1040 segmentFrom = intersection;
1041 }
1042 else
1043 {
1044 tempPoints.add(intersection);
1045 }
1046 }
1047 }
1048 else
1049 {
1050
1051 if (i == tempPoints.size() - 1)
1052 {
1053 tempPoints.remove(tempPoints.size() - 1);
1054 segmentFrom = tempPoints.get(tempPoints.size() - 1);
1055 tempPoints.remove(tempPoints.size() - 1);
1056 }
1057 }
1058 }
1059 pPoint = p;
1060 }
1061 }
1062 if (addSegment)
1063 {
1064 tempPoints.add(segmentFrom);
1065 tempPoints.add(segmentTo);
1066 prevPoint = nextPoint;
1067 prevAngle = angle;
1068 }
1069 }
1070
1071 for (int index = 1; index < tempPoints.size() - 1; index++)
1072 {
1073 Point2d checkPoint = tempPoints.get(index);
1074 prevPoint = null;
1075 boolean tooClose = false;
1076 boolean somewhereAtCorrectDistance = false;
1077 for (int i = 0; i < filteredReferenceLine.size(); i++)
1078 {
1079 Point2d p = filteredReferenceLine.get(i);
1080 if (null != prevPoint)
1081 {
1082 Point2d closestPoint = checkPoint.closestPointOnSegment(prevPoint, p);
1083 double distance = closestPoint.distance(checkPoint);
1084 if (distance < bufferOffset - circlePrecision)
1085 {
1086 tooClose = true;
1087 break;
1088 }
1089 else if (distance < bufferOffset + minimumOffset)
1090 {
1091 somewhereAtCorrectDistance = true;
1092 }
1093 }
1094 prevPoint = p;
1095 }
1096 if (tooClose || !somewhereAtCorrectDistance)
1097 {
1098 tempPoints.remove(index);
1099 index--;
1100 }
1101 }
1102 return new PolyLine2d(true, tempPoints);
1103 }
1104
1105 @Override
1106 public PolyLine2d offsetLine(final double offsetAtStart, final double offsetAtEnd, final double circlePrecision,
1107 final double offsetMinimumFilterValue, final double offsetMaximumFilterValue, final double offsetFilterRatio,
1108 final double minimumOffset) throws IllegalArgumentException, DrawRuntimeException
1109 {
1110 if (offsetAtStart == offsetAtEnd)
1111 {
1112 return offsetLine(offsetAtStart, circlePrecision, offsetMinimumFilterValue, offsetMaximumFilterValue,
1113 offsetFilterRatio, minimumOffset);
1114 }
1115 PolyLine2d atStart = offsetLine(offsetAtStart, circlePrecision, offsetMinimumFilterValue, offsetMaximumFilterValue,
1116 offsetFilterRatio, minimumOffset);
1117 PolyLine2d atEnd = offsetLine(offsetAtEnd, circlePrecision, offsetMinimumFilterValue, offsetMaximumFilterValue,
1118 offsetFilterRatio, minimumOffset);
1119 return atStart.transitionLine(atEnd, new TransitionFunction()
1120 {
1121 @Override
1122 public double function(final double fraction)
1123 {
1124 return fraction;
1125 }
1126 });
1127 }
1128
1129 @Override
1130 public PolyLine2d transitionLine(final PolyLine2d endLine, final TransitionFunction transition) throws DrawRuntimeException
1131 {
1132 Throw.whenNull(endLine, "endLine");
1133 Throw.whenNull(transition, "transition");
1134 List<Point2d> pointList = new ArrayList<>();
1135 int indexInStart = 0;
1136 int indexInEnd = 0;
1137 while (indexInStart < this.size() && indexInEnd < endLine.size())
1138 {
1139 double fractionInStart = lengthAtIndex(indexInStart) / this.length;
1140 double fractionInEnd = endLine.lengthAtIndex(indexInEnd) / endLine.length;
1141 if (fractionInStart < fractionInEnd)
1142 {
1143 pointList.add(get(indexInStart).interpolate(endLine.getLocation(fractionInStart * endLine.length),
1144 transition.function(fractionInStart)));
1145 indexInStart++;
1146 }
1147 else if (fractionInStart > fractionInEnd)
1148 {
1149 pointList.add(this.getLocation(fractionInEnd * this.length).interpolate(endLine.get(indexInEnd),
1150 transition.function(fractionInEnd)));
1151 indexInEnd++;
1152 }
1153 else
1154 {
1155 pointList.add(this.get(indexInStart).interpolate(endLine.getLocation(fractionInEnd * endLine.length),
1156 transition.function(fractionInStart)));
1157 indexInStart++;
1158 indexInEnd++;
1159 }
1160 }
1161 return new PolyLine2d(true, pointList);
1162 }
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175 public double projectRay(final Ray2d ray) throws NullPointerException
1176 {
1177 Throw.whenNull(ray, "ray");
1178 double bestDistance = Double.POSITIVE_INFINITY;
1179 double positionAtBestDistance = Double.NaN;
1180
1181
1182 double perpendicularX = ray.x - Math.sin(ray.dirZ);
1183 double perpendicularY = ray.y + Math.cos(ray.dirZ);
1184 for (int index = 1; index < this.x.length; index++)
1185 {
1186 Point2d intersection = Point2d.intersectionOfLines(ray.x, ray.y, perpendicularX, perpendicularY, false, false,
1187 this.x[index - 1], this.y[index - 1], this.x[index], this.y[index], true, true);
1188 if (intersection != null)
1189 {
1190 double thisDistance = intersection.distance(ray);
1191 if (thisDistance < bestDistance)
1192 {
1193 double distanceToPrevPoint =
1194 Math.hypot(intersection.x - this.x[index - 1], intersection.y - this.y[index - 1]);
1195 positionAtBestDistance = lengthAtIndex(index - 1) + distanceToPrevPoint;
1196 bestDistance = thisDistance;
1197 }
1198 }
1199 }
1200 return positionAtBestDistance;
1201 }
1202
1203
1204
1205
1206
1207 public Path2D toPath2D()
1208 {
1209 Path2D.Double result = new Path2D.Double();
1210 result.moveTo(this.x[0], this.y[0]);
1211 for (int i = 1; i < this.x.length; i++)
1212 {
1213 result.lineTo(this.x[i], this.y[i]);
1214 }
1215 return result;
1216 }
1217
1218 @Override
1219 public String toString()
1220 {
1221 return toString("%f", false);
1222 }
1223
1224 @Override
1225 public String toString(final String doubleFormat, final boolean doNotIncludeClassName)
1226 {
1227 StringBuilder result = new StringBuilder();
1228 if (!doNotIncludeClassName)
1229 {
1230 result.append("PolyLine2d ");
1231 }
1232 String format = String.format("%%sx=%1$s, y=%1$s", doubleFormat);
1233 for (int index = 0; index < this.x.length; index++)
1234 {
1235 result.append(String.format(Locale.US, format, index == 0 ? "[" : ", ", this.x[index], this.y[index]));
1236 }
1237 if (this.lengthIndexedLine.length == 1)
1238 {
1239 format = String.format(", startHeading=%1$s", doubleFormat);
1240 result.append(String.format(Locale.US, format, this.startHeading));
1241 }
1242 result.append("]");
1243 return result.toString();
1244 }
1245
1246 @Override
1247 public int hashCode()
1248 {
1249 final int prime = 31;
1250 int result = 1;
1251 long temp;
1252 temp = Double.doubleToLongBits(this.startHeading);
1253 result = prime * result + (int) (temp ^ (temp >>> 32));
1254 result = prime * result + Arrays.hashCode(this.x);
1255 result = prime * result + Arrays.hashCode(this.y);
1256 return result;
1257 }
1258
1259 @SuppressWarnings("checkstyle:needbraces")
1260 @Override
1261 public boolean equals(final Object obj)
1262 {
1263 if (this == obj)
1264 return true;
1265 if (obj == null)
1266 return false;
1267 if (getClass() != obj.getClass())
1268 return false;
1269 PolyLine2d other = (PolyLine2d) obj;
1270 if (Double.doubleToLongBits(this.startHeading) != Double.doubleToLongBits(other.startHeading))
1271 return false;
1272 if (!Arrays.equals(this.x, other.x))
1273 return false;
1274 if (!Arrays.equals(this.y, other.y))
1275 return false;
1276 return true;
1277 }
1278
1279 }