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