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