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