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