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
8 import org.djutils.draw.DrawException;
9 import org.djutils.draw.DrawRuntimeException;
10 import org.djutils.draw.Drawable3d;
11 import org.djutils.draw.Space3d;
12 import org.djutils.draw.bounds.Bounds3d;
13 import org.djutils.draw.point.OrientedPoint3d;
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, Space3d, OrientedPoint3d>
29 {
30
31 private static final long serialVersionUID = 20200911L;
32
33
34 private final Point3d[] points;
35
36
37 private final double[] lengthIndexedLine;
38
39
40 private final double length;
41
42
43 private final Bounds3d bounds;
44
45
46
47
48
49
50
51
52
53 private PolyLine3d(final boolean copyNeeded, final Point3d[] points) throws NullPointerException, DrawRuntimeException
54 {
55 Throw.whenNull(points, "points cannot be null");
56 Throw.when(points.length < 2, DrawRuntimeException.class, "Need at least two points");
57 this.points = copyNeeded ? Arrays.copyOf(points, points.length) : points;
58 Point3d prevPoint = points[0];
59 double minX = prevPoint.x;
60 double minY = prevPoint.y;
61 double minZ = prevPoint.z;
62 double maxX = prevPoint.x;
63 double maxY = prevPoint.y;
64 double maxZ = prevPoint.z;
65 this.lengthIndexedLine = new double[this.points.length];
66 this.lengthIndexedLine[0] = 0.0;
67 for (int i = 1; i < this.points.length; i++)
68 {
69 Point3d point = this.points[i];
70 minX = Math.min(minX, point.x);
71 minY = Math.min(minY, point.y);
72 minZ = Math.min(minZ, point.z);
73 maxX = Math.max(maxX, point.x);
74 maxY = Math.max(maxY, point.y);
75 maxZ = Math.max(maxZ, point.z);
76 if (prevPoint.x == point.x && prevPoint.y == point.y && prevPoint.z == point.z)
77 {
78 throw new DrawRuntimeException(
79 "Degenerate Line3d; point " + (i - 1) + " has the same x, y and z as point " + i);
80 }
81 this.lengthIndexedLine[i] = this.lengthIndexedLine[i - 1] + prevPoint.distance(point);
82 prevPoint = point;
83 }
84 this.length = this.lengthIndexedLine[this.lengthIndexedLine.length - 1];
85 this.bounds = new Bounds3d(minX, maxX, minY, maxY, minZ, maxZ);
86 }
87
88
89
90
91
92
93
94
95
96
97 public PolyLine3d(final Point3d.html#Point3d">Point3d point1, final Point3d point2, final Point3d... otherPoints)
98 throws NullPointerException, DrawRuntimeException
99 {
100 this(false, spliceArray(point1, point2, otherPoints));
101 }
102
103
104
105
106
107
108
109
110
111 private static Point3d.html#Point3d">Point3dPoint3d">Point3d[] spliceArray(final Point3d.html#Point3d">Point3d point1, final Point3d point2, final Point3d... otherPoints)
112 {
113 Point3dhtml#Point3d">Point3d[] result = new Point3d[2 + (otherPoints == null ? 0 : otherPoints.length)];
114 result[0] = point1;
115 result[1] = point2;
116 if (otherPoints != null)
117 {
118 for (int i = 0; i < otherPoints.length; i++)
119 {
120 result[i + 2] = otherPoints[i];
121 }
122 }
123 return result;
124 }
125
126
127
128
129
130
131
132
133 public PolyLine3d(final Point3d[] points) throws NullPointerException, DrawRuntimeException
134 {
135 this(true, checkLengthIsTwoOrMore(Throw.whenNull(points, "points may not be null")));
136 }
137
138
139
140
141
142
143
144 private static Point3dint3d[] checkLengthIsTwoOrMore(final Point3d[] points) throws DrawRuntimeException
145 {
146 Throw.when(points.length < 2, DrawRuntimeException.class, "Need at least two points");
147 return points;
148 }
149
150
151
152
153
154
155
156 public PolyLine3d(final Iterator<Point3d> iterator) throws NullPointerException, DrawException
157 {
158 this(iteratorToList(Throw.whenNull(iterator, "iterator cannot be null")));
159 }
160
161
162
163
164
165
166
167 public PolyLine3d(final List<Point3d> pointList) throws DrawRuntimeException
168 {
169 this(false, pointList.toArray(new Point3d[pointList.size()]));
170 }
171
172
173 @Override
174 public PolyLine3d instantiate(final List<Point3d> pointList) throws NullPointerException, DrawRuntimeException
175 {
176 return new PolyLine3d(pointList);
177 }
178
179
180
181
182
183
184 private static List<Point3d> iteratorToList(final Iterator<Point3d> iterator)
185 {
186 List<Point3d> result = new ArrayList<>();
187 iterator.forEachRemaining(result::add);
188 return result;
189 }
190
191
192 @Override
193 public int size()
194 {
195 return this.points.length;
196 }
197
198
199 @Override
200 public final Point3d get(final int i) throws IndexOutOfBoundsException
201 {
202 return this.points[i];
203 }
204
205
206 @Override
207 public final double lengthAtIndex(final int index)
208 {
209 return this.lengthIndexedLine[index];
210 }
211
212
213 @Override
214 public final double getLength()
215 {
216 return this.length;
217 }
218
219
220 @Override
221 public Iterator<Point3d> getPoints()
222 {
223 return Arrays.stream(this.points).iterator();
224 }
225
226
227 @Override
228 public Bounds3d getBounds()
229 {
230 return this.bounds;
231 }
232
233
234
235
236
237
238
239 public final PolyLine3d noiseFilteredLine(final double noiseLevel)
240 {
241 if (this.size() <= 2)
242 {
243 return this;
244 }
245 Point3d prevPoint = null;
246 List<Point3d> list = null;
247 for (int index = 0; index < this.size(); index++)
248 {
249 Point3d currentPoint = this.points[index];
250 if (null != prevPoint && prevPoint.distance(currentPoint) < noiseLevel)
251 {
252 if (null == list)
253 {
254
255 list = new ArrayList<>();
256 for (int i = 0; i < index; i++)
257 {
258 list.add(this.points[i]);
259 }
260 }
261 if (index == this.size() - 1)
262 {
263 if (list.size() > 1)
264 {
265
266 list.set(list.size() - 1, currentPoint);
267 }
268 else
269 {
270
271
272 list.add(currentPoint);
273 }
274 }
275 continue;
276 }
277 else if (null != list)
278 {
279 list.add(currentPoint);
280 }
281 prevPoint = currentPoint;
282 }
283 if (null == list)
284 {
285 return this;
286 }
287 if (list.size() == 2 && list.get(0).equals(list.get(1)))
288 {
289
290 list.add(1, this.points[1]);
291 }
292 try
293 {
294 return new PolyLine3d(list);
295 }
296 catch (DrawRuntimeException exception)
297 {
298 CategoryLogger.always().error(exception);
299 throw new Error(exception);
300 }
301 }
302
303
304
305
306
307
308
309
310 public static PolyLine3d concatenate(final PolyLine3d... lines) throws DrawException
311 {
312 return concatenate(0.0, lines);
313 }
314
315
316
317
318
319
320
321
322
323 public static PolyLine3d concatenate(final PolyLine3d.html#PolyLine3d">PolyLine3djxr_keyword">double tolerance, final PolyLine3d.html#PolyLine3d">PolyLine3d line1, final PolyLine3d line2)
324 throws DrawException
325 {
326 if (line1.getLast().distance(line2.getFirst()) > tolerance)
327 {
328 throw new DrawException("Lines are not connected: " + line1.getLast() + " to " + line2.getFirst() + " distance is "
329 + line1.getLast().distance(line2.getFirst()) + " > " + tolerance);
330 }
331 int size = line1.size() + line2.size() - 1;
332 Point3dhtml#Point3d">Point3d[] points = new Point3d[size];
333 int nextIndex = 0;
334 for (int j = 0; j < line1.size(); j++)
335 {
336 points[nextIndex++] = line1.get(j);
337 }
338 for (int j = 1; j < line2.size(); j++)
339 {
340 points[nextIndex++] = line2.get(j);
341 }
342 return new PolyLine3d(false, points);
343 }
344
345
346
347
348
349
350
351
352
353 public static PolyLine3d concatenate(final double tolerance, final PolyLine3d... lines) throws DrawException
354 {
355 if (0 == lines.length)
356 {
357 throw new DrawException("Empty argument list");
358 }
359 else if (1 == lines.length)
360 {
361 return lines[0];
362 }
363 int size = lines[0].size();
364 for (int i = 1; i < lines.length; i++)
365 {
366 if (lines[i - 1].getLast().distance(lines[i].getFirst()) > tolerance)
367 {
368 throw new DrawException("Lines are not connected: " + lines[i - 1].getLast() + " to " + lines[i].getFirst()
369 + " distance is " + lines[i - 1].getLast().distance(lines[i].getFirst()) + " > " + tolerance);
370 }
371 size += lines[i].size() - 1;
372 }
373 Point3dhtml#Point3d">Point3d[] points = new Point3d[size];
374 int nextIndex = 0;
375 for (int i = 0; i < lines.length; i++)
376 {
377 PolyLine3d line = lines[i];
378 for (int j = 0 == i ? 0 : 1; j < line.size(); j++)
379 {
380 points[nextIndex++] = line.get(j);
381 }
382 }
383 return new PolyLine3d(false, points);
384 }
385
386
387 @Override
388 public PolyLine2d project() throws DrawRuntimeException
389 {
390 List<Point2d> pointList = new ArrayList<>();
391 Point2d prevPoint = null;
392 for (Point3d point3d : this.points)
393 {
394 Point2d point = point3d.project();
395 if (prevPoint != null)
396 {
397 if (prevPoint.x == point.x && prevPoint.y == point.y)
398 {
399 continue;
400 }
401 }
402 pointList.add(point);
403 prevPoint = point;
404 }
405 return new PolyLine2d(pointList);
406 }
407
408
409
410
411
412
413
414 public static PolyLine3d createAndCleanLine3d(final Point3d... points) throws DrawException
415 {
416 if (points.length < 2)
417 {
418 throw new DrawException("Degenerate Line3d; has " + points.length + " point" + (points.length != 1 ? "s" : ""));
419 }
420 return createAndCleanLine3d(new ArrayList<>(Arrays.asList(points)));
421 }
422
423
424
425
426
427
428
429
430 public static PolyLine3d createAndCleanLine3d(final List<Point3d> pointList) throws DrawException
431 {
432
433
434 int i = 1;
435 while (i < pointList.size())
436 {
437 if (pointList.get(i - 1).equals(pointList.get(i)))
438 {
439 pointList.remove(i);
440 }
441 else
442 {
443 i++;
444 }
445 }
446 return new PolyLine3d(pointList);
447 }
448
449
450 @Override
451 public final OrientedPoint3d getLocationExtended(final double position)
452 {
453 if (position >= 0.0 && position <= getLength())
454 {
455 try
456 {
457 return getLocation(position);
458 }
459 catch (DrawException exception)
460 {
461
462 }
463 }
464
465
466 if (position < 0.0)
467 {
468 double len = position;
469 double fraction = len / (this.lengthIndexedLine[1] - this.lengthIndexedLine[0]);
470 Point3d p1 = this.points[0];
471 Point3d p2 = this.points[1];
472 return new OrientedPoint3d(p1.x + fraction * (p2.x - p1.x), p1.y + fraction * (p2.y - p1.y),
473 p1.z + fraction * (p2.z - p1.z), 0.0, 0.0, Math.atan2(p2.y - p1.y, p2.x - p1.x));
474 }
475
476
477
478 int n1 = this.lengthIndexedLine.length - 1;
479 int n2 = this.lengthIndexedLine.length - 2;
480 double len = position - getLength();
481 double fraction = len / (this.lengthIndexedLine[n1] - this.lengthIndexedLine[n2]);
482 while (Double.isInfinite(fraction))
483 {
484 if (--n2 < 0)
485 {
486 CategoryLogger.always().error("lengthIndexedLine of {} is invalid", this);
487 Point3d p = this.points[n1];
488 return new OrientedPoint3d(p.x, p.y, p.z, 0.0, 0.0, 0.0);
489 }
490 fraction = len / (this.lengthIndexedLine[n1] - this.lengthIndexedLine[n2]);
491 }
492 Point3d p1 = this.points[n2];
493 Point3d p2 = this.points[n1];
494 return new OrientedPoint3d(p2.x + fraction * (p2.x - p1.x), p2.y + fraction * (p2.y - p1.y),
495 p2.z + fraction * (p2.z - p1.z), 0.0, 0.0, Math.atan2(p2.y - p1.y, p2.x - p1.x));
496 }
497
498
499 @Override
500 public final OrientedPoint3d getLocation(final double position) throws DrawException
501 {
502 if (position < 0.0 || position > getLength())
503 {
504 throw new DrawException("getLocationSI for line: position < 0.0 or > line length. Position = " + position
505 + " m. Length = " + getLength() + " m.");
506 }
507
508 if (position == 0.0)
509 {
510 Point3d p1 = this.points[0];
511 Point3d p2 = this.points[1];
512 return new OrientedPoint3d(p1.x, p1.y, p1.z, 0.0, 0.0, Math.atan2(p2.y - p1.y, p2.x - p1.x));
513 }
514 if (position == getLength())
515 {
516 Point3d p1 = this.points[this.points.length - 2];
517 Point3d p2 = this.points[this.points.length - 1];
518 return new OrientedPoint3d(p2.x, p2.y, p2.z, 0.0, 0.0, Math.atan2(p2.y - p1.y, p2.x - p1.x));
519 }
520
521
522 int index = find(position);
523 double remainder = position - this.lengthIndexedLine[index];
524 double fraction = remainder / (this.lengthIndexedLine[index + 1] - this.lengthIndexedLine[index]);
525 Point3d p1 = this.points[index];
526 Point3d p2 = this.points[index + 1];
527 return new OrientedPoint3d(p1.x + fraction * (p2.x - p1.x), p1.y + fraction * (p2.y - p1.y),
528 p1.z + fraction * (p2.z - p1.z), 0.0, 0.0, Math.atan2(p2.y - p1.y, p2.x - p1.x));
529 }
530
531
532 @Override
533 public PolyLine3d extract(final double start, final double end) throws DrawException
534 {
535 if (Double.isNaN(start) || Double.isNaN(end) || start < 0 || start >= end || end > getLength())
536 {
537 throw new DrawException(
538 "Bad interval (" + start + ".." + end + "; length of this Line3d is " + this.getLength() + ")");
539 }
540 double cumulativeLength = 0;
541 double nextCumulativeLength = 0;
542 double segmentLength = 0;
543 int index = 0;
544 List<Point3d> pointList = new ArrayList<>();
545 while (start > cumulativeLength)
546 {
547 Point3d fromPoint = get(index);
548 index++;
549 Point3d toPoint = get(index);
550 segmentLength = fromPoint.distance(toPoint);
551 cumulativeLength = nextCumulativeLength;
552 nextCumulativeLength = cumulativeLength + segmentLength;
553 if (nextCumulativeLength >= start)
554 {
555 break;
556 }
557 }
558 if (start == nextCumulativeLength)
559 {
560 pointList.add(get(index));
561 }
562 else
563 {
564 pointList.add(get(index - 1).interpolate(get(index), (start - cumulativeLength) / segmentLength));
565 if (end > nextCumulativeLength)
566 {
567 pointList.add(get(index));
568 }
569 }
570 while (end > nextCumulativeLength)
571 {
572 Point3d fromPoint = get(index);
573 index++;
574 if (index >= size())
575 {
576 break;
577 }
578 Point3d toPoint = get(index);
579 segmentLength = fromPoint.distance(toPoint);
580 cumulativeLength = nextCumulativeLength;
581 nextCumulativeLength = cumulativeLength + segmentLength;
582 if (nextCumulativeLength >= end)
583 {
584 break;
585 }
586 pointList.add(toPoint);
587 }
588 if (end == nextCumulativeLength)
589 {
590 pointList.add(get(index));
591 }
592 else
593 {
594 Point3d point = get(index - 1).interpolate(get(index), (end - cumulativeLength) / segmentLength);
595
596 if (!point.equals(pointList.get(pointList.size() - 1)))
597 {
598 pointList.add(point);
599 }
600 }
601 try
602 {
603 return instantiate(pointList);
604 }
605 catch (DrawRuntimeException exception)
606 {
607 CategoryLogger.always().error(exception, "interval " + start + ".." + end + " too short");
608 throw new DrawException("interval " + start + ".." + end + "too short");
609 }
610 }
611
612
613 @Override
614 public PolyLine3d truncate(final double position) throws DrawException
615 {
616 if (position <= 0.0 || position > getLength())
617 {
618 throw new DrawException("truncate for line: position <= 0.0 or > line length. Position = " + position
619 + ". Length = " + getLength() + " m.");
620 }
621
622
623 if (position == getLength())
624 {
625 return this;
626 }
627
628
629 int index = find(position);
630 double remainder = position - lengthAtIndex(index);
631 double fraction = remainder / (lengthAtIndex(index + 1) - lengthAtIndex(index));
632 Point3d p1 = get(index);
633 Point3d lastPoint;
634 if (0.0 == fraction)
635 {
636 index--;
637 lastPoint = p1;
638 }
639 else
640 {
641 Point3d p2 = get(index + 1);
642 lastPoint = p1.interpolate(p2, fraction);
643
644 }
645
646 List<Point3d> coords = new ArrayList<>(index + 2);
647 for (int i = 0; i <= index; i++)
648 {
649 coords.add(get(i));
650 }
651 coords.add(lastPoint);
652 return instantiate(coords);
653 }
654
655
656 @Override
657 @SuppressWarnings("checkstyle:designforextension")
658 public int hashCode()
659 {
660 final int prime = 31;
661 int result = 1;
662 result = prime * result + Arrays.hashCode(this.points);
663 return result;
664 }
665
666
667 @Override
668 public String toString()
669 {
670 return "PolyLine3d [points=" + Arrays.toString(this.points) + "]";
671 }
672
673
674 @Override
675 @SuppressWarnings({"checkstyle:designforextension", "checkstyle:needbraces"})
676 public boolean equals(final Object obj)
677 {
678 if (this == obj)
679 return true;
680 if (obj == null)
681 return false;
682 if (getClass() != obj.getClass())
683 return false;
684 PolyLine3d../../../org/djutils/draw/line/PolyLine3d.html#PolyLine3d">PolyLine3d other = (PolyLine3d) obj;
685 if (!Arrays.equals(this.points, other.points))
686 return false;
687 return true;
688 }
689
690 }