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