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