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