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