1 /*
2 * Copyright (C) 2012 Alberto Irurueta Carro (alberto@irurueta.com)
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16 package com.irurueta.geometry;
17
18 import java.io.Serializable;
19 import java.util.ArrayList;
20 import java.util.List;
21
22 /**
23 * This class defines a polygon in 3D space.
24 */
25 @SuppressWarnings("DuplicatedCode")
26 public class Polygon3D implements Serializable {
27
28 /**
29 * Default threshold value. Thresholds are used to determine whether a point
30 * lies inside the polygon or not, or if it's locus or not, etc.
31 */
32 public static final double DEFAULT_THRESHOLD = 1e-9;
33
34 /**
35 * Minimum allowed threshold value.
36 */
37 public static final double MIN_THRESHOLD = 0.0;
38
39 /**
40 * Minimum number of vertices that a polygon is allowed to have.
41 */
42 public static final int MIN_VERTICES = 3;
43
44 /**
45 * Constant defining inhomogeneous coordinates.
46 */
47 public static final int INHOM_COORDS = 3;
48
49 /**
50 * Default method for triangulation.
51 */
52 public static final TriangulatorMethod DEFAULT_TRIANGULATOR_METHOD = TriangulatorMethod.VAN_GOGH_TRIANGULATOR;
53
54 /**
55 * List containing vertices of this polygon. Each vertex is a 3D point.
56 */
57 private List<Point3D> vertices;
58
59 /**
60 * Boolean indicating whether polygon has already been triangulated.
61 */
62 private boolean triangulated;
63
64 /**
65 * List containing triangles found after triangulating this polygon.
66 * Initially this list will be null until triangulation is done.
67 */
68 private List<Triangle3D> triangles;
69
70 /**
71 * Method to do triangulation.
72 */
73 private TriangulatorMethod triangulatorMethod;
74
75 /**
76 * Constructor.
77 *
78 * @param vertices List of vertices forming this polygon.
79 * @throws NotEnoughVerticesException Raised if list does not contain enough
80 * vertices.
81 * @see #MIN_VERTICES
82 */
83 public Polygon3D(final List<Point3D> vertices) throws NotEnoughVerticesException {
84 setVertices(vertices);
85 triangulatorMethod = DEFAULT_TRIANGULATOR_METHOD;
86 }
87
88 /**
89 * Returns triangulator method. Triangulator method determines the way a
90 * polygon is divided into triangles.
91 * If none has been provided DEFAULT_TRIANGULATOR_METHOD will be returned.
92 *
93 * @return Triangulator method.
94 */
95 public TriangulatorMethod getTriangulatorMethod() {
96 return triangulatorMethod;
97 }
98
99 /**
100 * Sets triangulator method. A triangulator method determines the way a
101 * polygon is divided into triangles.
102 *
103 * @param triangulatorMethod A triangulator method.
104 */
105 public void setTriangulatorMethod(final TriangulatorMethod triangulatorMethod) {
106 this.triangulatorMethod = triangulatorMethod;
107 }
108
109 /**
110 * Returns the list of vertices forming this polygon.
111 *
112 * @return List of vertices.
113 */
114 public List<Point3D> getVertices() {
115 return vertices;
116 }
117
118 /**
119 * Sets list of vertices forming this polygon.
120 *
121 * @param vertices List of vertices.
122 * @throws NotEnoughVerticesException Raised if provided list does not have
123 * enough vertices.
124 * @see #MIN_VERTICES
125 */
126 public final void setVertices(final List<Point3D> vertices) throws NotEnoughVerticesException {
127 if (vertices.size() < MIN_VERTICES) {
128 throw new NotEnoughVerticesException();
129 }
130
131 if (vertices instanceof Serializable) {
132 this.vertices = vertices;
133 } else {
134 this.vertices = new ArrayList<>(vertices);
135 }
136 triangulated = false;
137 triangles = null;
138 }
139
140 /**
141 * Determines whether this polygon has already been triangulated.
142 * A polygon will only need to be triangulated once, unless the list of
143 * vertices is reset.
144 *
145 * @return True if polygon has already been triangulated, false otherwise.
146 */
147 public boolean isTriangulated() {
148 return triangulated;
149 }
150
151 /**
152 * Returns a list of triangles forming this polygon.
153 * This method checks whether this polygon has already been triangulated,
154 * if not, it performs triangulation first.
155 *
156 * @return A list of triangles forming this polygon.
157 * @throws TriangulatorException Raised if triangulation was needed and
158 * failed.
159 */
160 public List<Triangle3D> getTriangles() throws TriangulatorException {
161 if (!isTriangulated()) {
162 triangulate();
163 }
164 return triangles;
165 }
166
167 /**
168 * Returns signed area of this polygon.
169 * The sign of the area determines whether vertices of the polygon are
170 * provided in clockwise (negative sign) or clockwise (positive sign) order.
171 *
172 * @return Signed area of this polygon.
173 */
174 public double getArea() {
175 final var origin = vertices.get(0);
176 final var inhomX0 = origin.getInhomX();
177 final var inhomY0 = origin.getInhomY();
178 final var inhomZ0 = origin.getInhomZ();
179
180 final var iterator = vertices.iterator();
181
182 // because there are at least 3
183 // vertices
184 var prevPoint = iterator.next();
185 Point3D curPoint;
186 var avgX = 0.0;
187 var avgY = 0.0;
188 var avgZ = 0.0;
189 while (iterator.hasNext()) {
190 curPoint = iterator.next();
191
192 final var inhomX1 = prevPoint.getInhomX();
193 final var inhomY1 = prevPoint.getInhomY();
194 final var inhomZ1 = prevPoint.getInhomZ();
195
196 final var inhomX2 = curPoint.getInhomX();
197 final var inhomY2 = curPoint.getInhomY();
198 final var inhomZ2 = curPoint.getInhomZ();
199
200 // compute cross product of ab = (prevPoint - origin) and
201 // ac = (curPoint - origin)
202 final var abX = inhomX1 - inhomX0;
203 final var abY = inhomY1 - inhomY0;
204 final var abZ = inhomZ1 - inhomZ0;
205
206 final var acX = inhomX2 - inhomX0;
207 final var acY = inhomY2 - inhomY0;
208 final var acZ = inhomZ2 - inhomZ0;
209
210 final var crossX = abY * acZ - abZ * acY;
211 final var crossY = abZ * acX - abX * acZ;
212 final var crossZ = abX * acY - abY * acX;
213
214 avgX += crossX;
215 avgY += crossY;
216 avgZ += crossZ;
217
218 prevPoint = curPoint;
219 }
220
221 return 0.5 * Math.sqrt(avgX * avgX + avgY * avgY + avgZ * avgZ);
222 }
223
224 /**
225 * Returns perimeter of this polygon.
226 * The perimeter is computed as the sum of the distances between consecutive
227 * pairs of vertices.
228 *
229 * @return Perimeter of this polygon.
230 */
231 public double getPerimeter() {
232 // iterate over all vertices and compute their distance
233 final var iterator = vertices.iterator();
234 var prevPoint = iterator.next();
235 Point3D point;
236 var perimeter = 0.0;
237 while (iterator.hasNext()) {
238 point = iterator.next();
239 perimeter += prevPoint.distanceTo(point);
240 prevPoint = point;
241 }
242 // get distance from last point with first one
243 perimeter += prevPoint.distanceTo(vertices.get(0));
244 return perimeter;
245 }
246
247 /**
248 * Determines if provided point lies within the region defined by this
249 * polygon.
250 * Notice that this method is only ensured to work for polygons having no
251 * holes or crossing borders. It will safely work on any other polygon,
252 * no matter if it is regular, non-regular, convex or concave.
253 *
254 * @param point Point to be checked.
255 * @return True if point lies within the area defined by this polygon, false
256 * otherwise.
257 * @throws TriangulatorException Raised if triangulation was required but
258 * failed.
259 */
260 public boolean isInside(final Point3D point) throws TriangulatorException {
261 return isInside(point, DEFAULT_THRESHOLD);
262 }
263
264 /**
265 * Determines if provided point lies within the region defined by this
266 * polygon.
267 * Notice that this method is only ensured to work for polygons having no
268 * holes or crossing borders. It will safely work on any other polygon,
269 * no matter if it is regular, non-regular, convex or concave.
270 *
271 * @param point Point to be checked.
272 * @param threshold Threshold to determine whether point lies inside this
273 * polygon. Usually this value should be small.
274 * @return True if point lies within the area defined by this polygon, false
275 * otherwise.
276 * @throws IllegalArgumentException Raised if provided threshold is negative.
277 * @throws TriangulatorException Raised if triangulation was required but
278 * failed.
279 */
280 public boolean isInside(final Point3D point, final double threshold)
281 throws TriangulatorException {
282 if (threshold < MIN_THRESHOLD) {
283 throw new IllegalArgumentException();
284 }
285
286 for (final var triangle : getTriangles()) {
287 if (triangle.isInside(point, threshold)) {
288 return true;
289 }
290 }
291 return false;
292 }
293
294 /**
295 * Returns the center of this polygon.
296 * The center is the average point among all the vertices of this polygon.
297 * The center is not ensure to lie within the area formed by this polygon.
298 *
299 * @return Center of this polygon.
300 */
301 public Point3D getCenter() {
302 final var result = Point3D.create();
303 center(result);
304 return result;
305 }
306
307 /**
308 * Computes the center of this polygon.
309 * The center is the average point among all the vertices of this polygon.
310 * The center is not ensured to lie within the area formed by this polygon.
311 *
312 * @param result Instance where the computed center will be stored.
313 */
314 public void center(final Point3D result) {
315 // compute average location of all vertices
316 var inhomX = 0.0;
317 var inhomY = 0.0;
318 var inhomZ = 0.0;
319 final var total = vertices.size();
320
321 for (final var point : vertices) {
322 inhomX += point.getInhomX() / total;
323 inhomY += point.getInhomY() / total;
324 inhomZ += point.getInhomZ() / total;
325 }
326 result.setInhomogeneousCoordinates(inhomX, inhomY, inhomZ);
327 }
328
329 /**
330 * Determines whether provided point is locus of the borders defined by
331 * the vertices of this polygon. A point will be locus if it lies in the
332 * line defined by two consecutive vertices up to a certain threshold of
333 * error.
334 *
335 * @param point Point to be checked.
336 * @param threshold Threshold of allowed error. This should usually be a
337 * small value.
338 * @return True if provided point lies in a border of this polygon, false
339 * otherwise.
340 * @throws IllegalArgumentException Raised if provided threshold is negative.
341 */
342 public boolean isLocus(final Point3D point, final double threshold) {
343 if (threshold < MIN_THRESHOLD) {
344 throw new IllegalArgumentException();
345 }
346
347 // normalize point to increase accuracy
348 point.normalize();
349
350 final var iterator = vertices.iterator();
351 // it's ok because there are at
352 // least 3 vertices
353 var prevPoint = iterator.next();
354 // normalize to increase accuracy
355 prevPoint.normalize();
356
357 Point3D curPoint;
358 while (iterator.hasNext()) {
359 curPoint = iterator.next();
360 // normalize to increase accuracy
361 curPoint.normalize();
362 if (point.isBetween(prevPoint, curPoint, threshold)) {
363 return true;
364 }
365 prevPoint = curPoint;
366 }
367
368 // check last point with first
369 return point.isBetween(prevPoint, vertices.get(0), threshold);
370 }
371
372 /**
373 * Determines whether provided point is locus of the borders defined by the
374 * vertices of this polygon. A point will be locus if it lies in the line
375 * defined by two consecutive vertices.
376 *
377 * @param point Point to be checked.
378 * @return True if provided point lies in a border of this polygon, false
379 * otherwise.
380 */
381 public boolean isLocus(final Point3D point) {
382 return isLocus(point, DEFAULT_THRESHOLD);
383 }
384
385 /**
386 * Returns the shortest distance from provided point to a border of this
387 * polygon. Note that borders are segments defined by consecutive vertices
388 *
389 * @param point Point to be checked.
390 * @return Shortest distance from provided point to this polygon.
391 * @throws CoincidentPointsException Raised if points in a polygon are too
392 * close. This usually indicates numerical instability or polygon degeneracy.
393 */
394 public double getShortestDistance(final Point3D point) throws CoincidentPointsException {
395 // iterate over all vertices and compute their distance
396 var iterator = vertices.iterator();
397 var prevPoint = iterator.next();
398 // to increase accuracy
399 prevPoint.normalize();
400 Point3D curPoint;
401 var bestDist = Double.MAX_VALUE;
402 double dist;
403 var found = false;
404 Line3D line = null;
405 final var pointInLine = Point3D.create();
406
407 while (iterator.hasNext()) {
408 curPoint = iterator.next();
409 // to increase accuracy
410 curPoint.normalize();
411
412 // check if point lies in the segment of the boundary of this polygon
413 if (point.isBetween(curPoint, prevPoint)) {
414 return 0.0;
415 }
416
417 if (line == null) {
418 line = new Line3D(curPoint, prevPoint);
419 } else {
420 line.setPlanesFromPoints(curPoint, prevPoint);
421 }
422 // to increase accuracy
423 line.normalize();
424
425 // find the closest point to line
426 line.closestPoint(point, pointInLine);
427 // to increase accuracy
428 pointInLine.normalize();
429
430 if (pointInLine.isBetween(curPoint, prevPoint)) {
431 // closest point lies within segment of polygon boundary, so we
432 // keep distance
433 dist = point.distanceTo(pointInLine);
434 if (dist < bestDist) {
435 // a better point has been found
436 bestDist = dist;
437 found = true;
438 }
439 }
440
441 prevPoint = curPoint;
442 }
443
444 // try last vertex with first
445 // check if point lies in the segment of the boundary of this polygon
446 final var first = vertices.get(0);
447 if (point.isBetween(prevPoint, first)) {
448 return 0.0;
449 }
450
451 if (line == null) {
452 line = new Line3D(prevPoint, first);
453 } else {
454 line.setPlanesFromPoints(prevPoint, first);
455 }
456 // to increase accuracy
457 line.normalize();
458
459 // find the closest point to line
460 line.closestPoint(point, pointInLine);
461 // to increase accuracy
462 pointInLine.normalize();
463
464 if (pointInLine.isBetween(prevPoint, first)) {
465 // closest point lies within segment of polygon boundary, so we
466 // keep distance
467 dist = point.distanceTo(pointInLine);
468 if (dist < bestDist) {
469 // a better point has been found
470 bestDist = dist;
471 found = true;
472 }
473 }
474
475 if (!found) {
476 // no closest point was found on a segment belonging to polygon
477 // boundary, so we search for the closest vertex
478 iterator = vertices.iterator();
479 while (iterator.hasNext()) {
480 // a better vertex has been found
481 curPoint = iterator.next();
482 dist = point.distanceTo(curPoint);
483 if (dist < bestDist) {
484 bestDist = dist;
485 }
486 }
487 }
488
489 return bestDist;
490 }
491
492 /**
493 * Returns the closest point to provided point that is locus of this
494 * polygon (i.e. lies on a border of this polygon).
495 *
496 * @param point Point to be checked.
497 * @return Closest point being locus of this polygon.
498 * @throws CoincidentPointsException Raised if points in a polygon are too
499 * close. This usually indicates numerical instability or polygon degeneracy.
500 */
501 public Point3D getClosestPoint(final Point3D point) throws CoincidentPointsException {
502 final var result = Point3D.create();
503 closestPoint(point, result);
504 return result;
505 }
506
507 /**
508 * Computes the closes point to provided point that is locus of this
509 * polygon (i.e. lies on a border of this polygon).
510 *
511 * @param point Point to be checked.
512 * @param result Instance where the closest point will be stored.
513 * @throws CoincidentPointsException Raised if points in a polygon are too
514 * close. This usually indicates numerical instability or polygon degeneracy.
515 */
516 public void closestPoint(final Point3D point, final Point3D result) throws CoincidentPointsException {
517 // iterate over all vertices and compute their distance
518 var iterator = vertices.iterator();
519 var prevPoint = iterator.next();
520 // to increase accuracy
521 prevPoint.normalize();
522
523 Point3D curPoint;
524 var bestDist = Double.MAX_VALUE;
525 double dist;
526 var found = false;
527 Line3D line = null;
528 final var pointInLine = Point3D.create();
529
530 while (iterator.hasNext()) {
531 curPoint = iterator.next();
532 // to increase accuracy
533 curPoint.normalize();
534
535 // check if point lies in the segment of the boundary of this polygon
536 if (point.isBetween(curPoint, prevPoint)) {
537 result.setCoordinates(point);
538 return;
539 }
540
541 if (line == null) {
542 line = new Line3D(curPoint, prevPoint);
543 } else {
544 line.setPlanesFromPoints(curPoint, prevPoint);
545 }
546 // to increase accuracy
547 line.normalize();
548
549 // find the closest point to line
550 line.closestPoint(point, pointInLine);
551 // to increase accuracy
552 pointInLine.normalize();
553
554 if (pointInLine.isBetween(curPoint, prevPoint)) {
555 // closest point lies within segment of polygon boundary, so we
556 // keep distance and point
557 dist = point.distanceTo(pointInLine);
558 if (dist < bestDist) {
559 // a better point has been found
560 bestDist = dist;
561 result.setCoordinates(pointInLine);
562 found = true;
563 }
564 }
565
566 prevPoint = curPoint;
567 }
568
569 // try last vertex with first
570 // check if point lies in the segment of the boundary of this polygon
571 final var first = vertices.get(0);
572 if (point.isBetween(prevPoint, first)) {
573 result.setCoordinates(point);
574 return;
575 }
576
577 if (line == null) {
578 line = new Line3D(prevPoint, first);
579 } else {
580 line.setPlanesFromPoints(prevPoint, first);
581 }
582 // to increase accuracy
583 line.normalize();
584
585 // find the closest point to line
586 line.closestPoint(point, pointInLine);
587 // to increase accuracy
588 pointInLine.normalize();
589
590 if (pointInLine.isBetween(prevPoint, first)) {
591 // closest point lies within segment of polygon boundary, so we
592 // keep distance
593 dist = point.distanceTo(pointInLine);
594 if (dist < bestDist) {
595 // a better point has been found
596 bestDist = dist;
597 result.setCoordinates(pointInLine);
598 found = true;
599 }
600 }
601
602
603 if (!found) {
604 // no closest point was found on a segment belonging to polygon
605 // boundary, so we search for the closest vertex
606 iterator = vertices.iterator();
607 while (iterator.hasNext()) {
608 curPoint = iterator.next();
609 dist = point.distanceTo(curPoint);
610 if (dist < bestDist) {
611 // a better vertex has been found
612 bestDist = dist;
613 result.setCoordinates(curPoint);
614 }
615 }
616 }
617 }
618
619 /**
620 * Triangulates this polygon using this polygon's triangulator method.
621 * A polygon only will be triangulated once when required or this method is
622 * called.
623 * This method will make no action if a polygon is already triangulated
624 * unless it's vertices are reset.
625 *
626 * @throws TriangulatorException Raised if triangulation failed.
627 * @see #getTriangulatorMethod
628 * @see #setTriangulatorMethod(TriangulatorMethod)
629 */
630 public void triangulate() throws TriangulatorException {
631 if (!triangulated) {
632 final var triangulator = Triangulator3D.create(triangulatorMethod);
633 triangles = triangulator.triangulate(vertices);
634 triangulated = true;
635 }
636 }
637
638 /**
639 * Computes the average orientation of a 3D polygon as an array
640 * containing the vector coordinates of the orientation.
641 * Notice that if all the vertices of the polygon lie in the same plane,
642 * then the estimated orientation will be exact, otherwise an average
643 * orientation will be estimated.
644 * Estimated orientation will be normalized (the norm of the estimated
645 * vector will be 1).
646 *
647 * @param vertices List of vertices forming a polygon in 3D.
648 * @param result Array where the estimated orientation will be stored.
649 * @param threshold Threshold to determine whether the orientation can be
650 * estimated or not. Because the estimated orientation needs to be
651 * normalized, it will only be possible to do so when norm has a reasonable
652 * value. If estimated norm happens to be smaller than provided value, then
653 * it will be assumed that polygon contains a degeneracy or that a
654 * consecutive pair of vertices are coincident.
655 * @throws IllegalArgumentException Raised if provided result array does not
656 * have length 3, or if the list of provided vertices does not contain at
657 * least three vertices, or if provided threshold is negative.
658 * @throws CoincidentPointsException Raised usually when it is determined
659 * that consecutive vertices of the polygon are too close (i.e. coincident)
660 * or when there are polygon degeneracies or numerical instabilities.
661 */
662 public static void orientation(
663 final List<Point3D> vertices, final double[] result, final double threshold)
664 throws CoincidentPointsException {
665 final var numVertices = vertices.size();
666 if (numVertices < MIN_VERTICES) {
667 throw new IllegalArgumentException();
668 }
669 if (result.length != INHOM_COORDS) {
670 throw new IllegalArgumentException();
671 }
672 if (threshold < MIN_THRESHOLD) {
673 throw new IllegalArgumentException();
674 }
675
676 var avgX = 0.0;
677 var avgY = 0.0;
678 var avgZ = 0.0;
679
680 final var iterator = vertices.iterator();
681 var prevPoint = iterator.next();
682 var origin = prevPoint;
683 Point3D curPoint;
684 final var inhomX0 = origin.getInhomX();
685 final var inhomY0 = origin.getInhomY();
686 final var inhomZ0 = origin.getInhomZ();
687
688 while (iterator.hasNext()) {
689 curPoint = iterator.next();
690 final var inhomX1 = prevPoint.getInhomX();
691 final var inhomY1 = prevPoint.getInhomY();
692 final var inhomZ1 = prevPoint.getInhomZ();
693
694 final var inhomX2 = curPoint.getInhomX();
695 final var inhomY2 = curPoint.getInhomY();
696 final var inhomZ2 = curPoint.getInhomZ();
697
698 // compute cross product of ab = (vertex1 - origin) and ac = (vertex2
699 // - origin)
700 final var abX = inhomX1 - inhomX0;
701 final var abY = inhomY1 - inhomY0;
702 final var abZ = inhomZ1 - inhomZ0;
703
704 final var acX = inhomX2 - inhomX0;
705 final var acY = inhomY2 - inhomY0;
706 final var acZ = inhomZ2 - inhomZ0;
707
708 final var crossX = abY * acZ - abZ * acY;
709 final var crossY = abZ * acX - abX * acZ;
710 final var crossZ = abX * acY - abY * acX;
711
712 avgX += crossX;
713 avgY += crossY;
714 avgZ += crossZ;
715
716 prevPoint = curPoint;
717 }
718
719 final var norm = Math.sqrt(avgX * avgX + avgY * avgY + avgZ * avgZ);
720
721 if (norm < threshold) {
722 throw new CoincidentPointsException();
723 }
724
725 avgX /= norm;
726 avgY /= norm;
727 avgZ /= norm;
728
729 result[0] = avgX;
730 result[1] = avgY;
731 result[2] = avgZ;
732 }
733
734 /**
735 * Computes the average orientation of a 3D polygon as an array
736 * containing the vector coordinates of the orientation.
737 * Notice that if all the vertices of the polygon lie in the same plane,
738 * then the estimated orientation will be exact, otherwise an average
739 * orientation will be estimated.
740 * Estimated orientation will be normalized (the norm of the estimated
741 * vector will be 1).
742 *
743 * @param vertices List of vertices forming a polygon in 3D.
744 * @param result Array where the estimated orientation will be stored.
745 * @throws IllegalArgumentException Raised if provided result array does not
746 * have length 3, or if the list of provided vertices does not contain at
747 * least three vertices.
748 * @throws CoincidentPointsException Raised usually when it is determined
749 * that consecutive vertices of the polygon are too close (i.e. coincident)
750 * or when there are polygon degeneracies or numerical instabilities.
751 */
752 public static void orientation(
753 final List<Point3D> vertices, final double[] result) throws CoincidentPointsException {
754 orientation(vertices, result, DEFAULT_THRESHOLD);
755 }
756
757 /**
758 * Returns the average orientation of a 3D polygon as an array containing
759 * the vector coordinates of the orientation.
760 * Notice that if all the vertices of the polygon lie in the same plane,
761 * then the estimated orientation will be exact, otherwise an average
762 * orientation will be estimated.
763 * Estimated orientation will be normalized (the norm of the estimated
764 * vector will be 1).
765 *
766 * @param vertices List of vertices forming a polygon in 3D.
767 * @param threshold Threshold to determine whether the orientation can be
768 * estimated or not. Because the estimated orientation needs to be
769 * normalized, it will only be possible to do so when norm has a reasonable
770 * value. If estimated norm happens to be smaller than provided value, then
771 * it will be assumed that polygon contains a degeneracy or that a
772 * consecutive pair of vertices are coincident.
773 * @return Array containing estimated orientation.
774 * @throws IllegalArgumentException Raised if the list of provided vertices
775 * does not contain at least three vertices, or if provided threshold is
776 * negative.
777 * @throws CoincidentPointsException Raised usually when it is determined
778 * that consecutive vertices of the polygon are too close (i.e. coincident)
779 * or when there are polygon degeneracies or numerical instabilities.
780 */
781 public static double[] orientation(
782 final List<Point3D> vertices, final double threshold) throws CoincidentPointsException {
783 final var out = new double[INHOM_COORDS];
784 orientation(vertices, out, threshold);
785 return out;
786 }
787
788 /**
789 * Returns the average orientation of a 3D polygon as an array containing
790 * the vector coordinates of the orientation.
791 * Notice that if all the vertices of the polygon lie in the same plane,
792 * then the estimated orientation will be exact, otherwise an average
793 * orientation will be estimated.
794 * Estimated orientation will be normalized (the norm of the estimated
795 * vector will be 1).
796 *
797 * @param vertices List of vertices forming a polygon in 3D
798 * @return Array containing estimated orientation.
799 * @throws IllegalArgumentException Raised if the list of provided vertices
800 * does not contain at least three vertices.
801 * @throws CoincidentPointsException Raised usually when it is determined
802 * that consecutive vertices of the polygon are too close (i.e. coincident)
803 * or when there are polygon degeneracies or numerical instabilities.
804 */
805 public static double[] orientation(final List<Point3D> vertices) throws CoincidentPointsException {
806 return orientation(vertices, DEFAULT_THRESHOLD);
807 }
808
809 /**
810 * Computes the average orientation of provided 3D polygon as an array
811 * containing the vector coordinates of the orientation.
812 * Notice that if all the vertices of the polygon lie in the same plane,
813 * then the estimated orientation will be exact, otherwise an average
814 * orientation will be estimated.
815 * Estimated orientation will be normalized (the norm of the estimated
816 * vector will be 1).
817 *
818 * @param polygon A polygon in 3D.
819 * @param result Array where the estimated orientation will be stored.
820 * @param threshold Threshold to determine whether the orientation can be
821 * estimated or not. Because the estimated orientation needs to be
822 * normalized, it will only be possible to do so when norm has a reasonable
823 * value. If estimated norm happens to be smaller than provided value, then
824 * it will be assumed that polygon contains a degeneracy or that a
825 * consecutive pair of vertices are coincident.
826 * @throws IllegalArgumentException Raised if provided result array does not
827 * have length 3 or if provided threshold is negative.
828 * @throws CoincidentPointsException Raised usually when it is determined
829 * that consecutive vertices of the polygon are too close (i.e. coincident)
830 * or when there are polygon degeneracies or numerical instabilities.
831 */
832 public static void orientation(
833 final Polygon3D polygon, final double[] result, final double threshold) throws CoincidentPointsException {
834 orientation(polygon.getVertices(), result, threshold);
835 }
836
837 /**
838 * Computes the average orientation of provided 3D polygon as an array
839 * containing the vector coordinates of the orientation.
840 * Notice that if all the vertices of the polygon lie in the same plane,
841 * then the estimated orientation will be exact, otherwise an average
842 * orientation will be estimated.
843 * Estimated orientation will be normalized (the norm of the estimated
844 * vector will be 1).
845 *
846 * @param polygon A polygon in 3D.
847 * @param result Array where the estimated orientation will be stored.
848 * @throws IllegalArgumentException Raised if provided result array does not
849 * have length 3.
850 * @throws CoincidentPointsException Raised usually when it is determined
851 * that consecutive vertices of the polygon are too close (i.e. coincident)
852 * or when there are polygon degeneracies or numerical instabilities.
853 */
854 public static void orientation(
855 final Polygon3D polygon, final double[] result) throws CoincidentPointsException {
856 orientation(polygon, result, DEFAULT_THRESHOLD);
857 }
858
859 /**
860 * Returns the average orientation of provided 3D polygon as an array
861 * containing the vector coordinates of the orientation.
862 * Notice that if all the vertices of the polygon lie in the same plane,
863 * then the estimated orientation will be exact, otherwise an average
864 * orientation will be estimated.
865 * Estimated orientation will be normalized (the norm of the estimated
866 * vector will be 1).
867 *
868 * @param polygon A polygon in 3D.
869 * @param threshold Threshold to determine whether the orientation can be
870 * estimated or not. Because the estimated orientation needs to be
871 * normalized, it will only be possible to do so when norm has a reasonable
872 * value. If estimated norm happens to be smaller than provided value, then
873 * it will be assumed that polygon contains a degeneracy or that a
874 * consecutive pair of vertices are coincident.
875 * @return Array containing estimated orientation.
876 * @throws IllegalArgumentException Raised if provided threshold is
877 * negative.
878 * @throws CoincidentPointsException Raised usually when it is determined
879 * that consecutive vertices of the polygon are too close (i.e. coincident)
880 * or when there are polygon degeneracies or numerical instabilities.
881 */
882 public static double[] orientation(
883 final Polygon3D polygon, final double threshold) throws CoincidentPointsException {
884 return orientation(polygon.getVertices(), threshold);
885 }
886
887 /**
888 * Returns the average orientation of provided 3D polygon as an array
889 * containing the vector coordinates of the orientation.
890 * Notice that if all the vertices of the polygon lie in the same plane,
891 * then the estimated orientation will be exact, otherwise an average
892 * orientation will be estimated.
893 * Estimated orientation will be normalized (the norm of the estimated
894 * vector will be 1).
895 *
896 * @param polygon A polygon in 3D.
897 * @return Array containing estimated orientation.
898 * @throws CoincidentPointsException Raised usually when it is determined
899 * that consecutive vertices of the polygon are too close (i.e. coincident)
900 * or when there are polygon degeneracies or numerical instabilities.
901 */
902 public static double[] orientation(final Polygon3D polygon) throws CoincidentPointsException {
903 return orientation(polygon, DEFAULT_THRESHOLD);
904 }
905
906 /**
907 * Computes the average orientation of this polygon as an array containing
908 * the vector coordinates of the orientation.
909 * Notice that if all the vertices of the polygon lie in the same plane,
910 * then the estimated orientation will be exact, otherwise an average
911 * orientation will be estimated.
912 * Estimated orientation will be normalized (the norm of the estimated
913 * vector will be 1).
914 *
915 * @param result Array where the estimated orientation will be stored.
916 * @param threshold Threshold to determine whether the orientation can be
917 * estimated or not. Because the estimated orientation needs to be
918 * normalized, it will only be possible to do so when norm has a reasonable
919 * value. If estimated norm happens to be smaller than provided value, then
920 * it will be assumed that polygon contains a degeneracy or that a
921 * consecutive pair of vertices are coincident.
922 * @throws IllegalArgumentException Raised if provided result array does not
923 * have length 3 or if provided threshold is negative.
924 * @throws CoincidentPointsException Raised usually when it is determined
925 * that consecutive vertices of the polygon are too close (i.e. coincident)
926 * or when there are polygon degeneracies or numerical instabilities.
927 */
928 public void orientation(final double[] result, final double threshold) throws CoincidentPointsException {
929 orientation(vertices, result, threshold);
930 }
931
932 /**
933 * Computes the average orientation of provided 3D polygon as an array
934 * containing the vector coordinates of the orientation.
935 * Notice that if all the vertices of the polygon lie in the same plane,
936 * then the estimated orientation will be exact, otherwise an average
937 * orientation will be estimated.
938 * Estimated orientation will be normalized (the norm of the estimated
939 * vector will be 1).
940 *
941 * @param result Array where the estimated orientation will be stored.
942 * @throws IllegalArgumentException Raised if provided result array does not
943 * have length 3.
944 * @throws CoincidentPointsException Raised usually when it is determined
945 * that consecutive vertices of the polygon are too close (i.e. coincident)
946 * or when there are polygon degeneracies or numerical instabilities.
947 */
948 public void orientation(final double[] result) throws CoincidentPointsException {
949 orientation(result, DEFAULT_THRESHOLD);
950 }
951
952 /**
953 * Returns the average orientation of provided 3D polygon as an array
954 * containing the vector coordinates of the orientation.
955 * Notice that if all the vertices of the polygon lie in the same plane,
956 * then the estimated orientation will be exact, otherwise an average
957 * orientation will be estimated.
958 * Estimated orientation will be normalized (the norm of the estimated
959 * vector will be 1).
960 *
961 * @param threshold Threshold to determine whether the orientation can be
962 * estimated or not. Because the estimated orientation needs to be
963 * normalized, it will only be possible to do so when norm has a reasonable
964 * value. If estimated norm happens to be smaller than provided value, then
965 * it will be assumed that polygon contains a degeneracy or that a
966 * consecutive pair of vertices are coincident.
967 * @return Array containing estimated orientation.
968 * @throws IllegalArgumentException Raised if provided threshold is
969 * negative.
970 * @throws CoincidentPointsException Raised usually when it is determined
971 * that consecutive vertices of the polygon are too close (i.e. coincident)
972 * or when there are polygon degeneracies or numerical instabilities.
973 */
974 public double[] getOrientation(final double threshold) throws CoincidentPointsException {
975 return orientation(vertices, threshold);
976 }
977
978 /**
979 * Returns the average orientation of provided 3D polygon as an array
980 * containing the vector coordinates of the orientation.
981 * Notice that if all the vertices of the polygon lie in the same plane,
982 * then the estimated orientation will be exact, otherwise an average
983 * orientation will be estimated.
984 * Estimated orientation will be normalized (the norm of the estimated
985 * vector will be 1).
986 *
987 * @return Array containing estimated orientation.
988 * @throws CoincidentPointsException Raised usually when it is determined
989 * that consecutive vertices of the polygon are too close (i.e. coincident)
990 * or when there are polygon degeneracies or numerical instabilities.
991 */
992 public double[] getOrientation() throws CoincidentPointsException {
993 return getOrientation(DEFAULT_THRESHOLD);
994 }
995
996 /**
997 * Returns the angle between two polygons, assuming that all vertices of
998 * each polygon lie on a given plane. Hence, this is equivalent to
999 * estimating the angle between the planes formed by two polygons.
1000 * The angle between two polygons is estimated by first estimating their
1001 * orientation.
1002 *
1003 * @param polygon1 1st polygon.
1004 * @param polygon2 2nd polygon.
1005 * @param threshold Threshold to determine when polygon orientation can be
1006 * estimated.
1007 * @return Angle between two polygons in radians.
1008 * @throws IllegalArgumentException Raised if provided threshold is negative.
1009 * @throws CoincidentPointsException Raised usually when it is determined
1010 * that consecutive vertices of the polygon are too close (i.e. coincident)
1011 * or when there are polygon degeneracies or numerical instabilities.
1012 * @see #orientation(Polygon3D, double[])
1013 */
1014 public static double getAngleBetweenPolygons(
1015 final Polygon3D polygon1, final Polygon3D polygon2, final double threshold)
1016 throws CoincidentPointsException {
1017 return getAngleBetweenPolygons(polygon1.getVertices(), polygon2.getVertices(), threshold);
1018 }
1019
1020 /**
1021 * Returns the angle between two polygons, assuming that all vertices of
1022 * each polygon lie on a given plane. Hence, this is equivalent to
1023 * estimating the angle between the planes formed by two polygons.
1024 * The angle between two polygons is estimated by first estimating their
1025 * orientation.
1026 *
1027 * @param polygon1 1st polygon.
1028 * @param polygon2 2nd polygon.
1029 * @return Angle between two polygons in radians.
1030 * @throws CoincidentPointsException Raised usually when it is determined
1031 * that consecutive vertices of the polygon are too close (i.e. coincident)
1032 * or when there are polygon degeneracies or numerical instabilities.
1033 * @see #orientation(Polygon3D, double[])
1034 */
1035 public static double getAngleBetweenPolygons(final Polygon3D polygon1, final Polygon3D polygon2)
1036 throws CoincidentPointsException {
1037 return getAngleBetweenPolygons(polygon1, polygon2, DEFAULT_THRESHOLD);
1038 }
1039
1040 /**
1041 * Returns the angle between two polygons formed each of them by the
1042 * corresponding list of provided vertices and assuming that all vertices of
1043 * each polygon lie on a given plane. Hence, this is equivalent to
1044 * estimating the angle between the planes formed by two polygons.
1045 * The angle between two polygons is estimated by first estimating their
1046 * orientation.
1047 *
1048 * @param vertices1 Vertices of 1st polygon.
1049 * @param vertices2 2nd polygon.
1050 * @param threshold Threshold to determine when polygon orientation can be
1051 * estimated.
1052 * @return Angle between two polygons in radians.
1053 * @throws IllegalArgumentException Raised if provided threshold is negative
1054 * or if list of vertices do not contain at least 3 vertices for each
1055 * polygon.
1056 * @throws CoincidentPointsException Raised usually when it is determined
1057 * that consecutive vertices of the polygon are too close (i.e. coincident)
1058 * or when there are polygon degeneracies or numerical instabilities.
1059 * @see #orientation(Polygon3D, double[])
1060 */
1061 public static double getAngleBetweenPolygons(
1062 final List<Point3D> vertices1, final List<Point3D> vertices2, final double threshold)
1063 throws CoincidentPointsException {
1064 return getAngleBetweenOrientations(Polygon3D.orientation(vertices1, threshold),
1065 Polygon3D.orientation(vertices2, threshold));
1066 }
1067
1068 /**
1069 * Returns the angle between two polygons formed each of them by the
1070 * corresponding list of provided vertices and assuming that all vertices of
1071 * each polygon lie on a given plane. Hence, this is equivalent to
1072 * estimating the angle between the planes formed by two polygons.
1073 * The angle between two polygons is estimated by first estimating their
1074 * orientation.
1075 *
1076 * @param vertices1 Vertices of 1st polygon.
1077 * @param vertices2 2nd polygon.
1078 * @return Angle between two polygons in radians.
1079 * @throws IllegalArgumentException Raised if list of vertices do not
1080 * contain at least 3 vertices for each polygon.
1081 * @throws CoincidentPointsException Raised usually when it is determined
1082 * that consecutive vertices of the polygon are too close (i.e. coincident)
1083 * or when there are polygon degeneracies or numerical instabilities.
1084 * @see #orientation(Polygon3D, double[])
1085 */
1086 public static double getAngleBetweenPolygons(
1087 final List<Point3D> vertices1, final List<Point3D> vertices2) throws CoincidentPointsException {
1088 return getAngleBetweenPolygons(vertices1, vertices2, DEFAULT_THRESHOLD);
1089 }
1090
1091 /**
1092 * Internal method to compute polygon orientation from their respective
1093 * orientation vectors.
1094 * Orientation vectors are provided as arrays and can be obtained by calling
1095 * orientation(Polygon3D, double[]) among other methods.
1096 *
1097 * @param orientation1 Orientation of 1st polygon.
1098 * @param orientation2 Orientation of 2nd polygon.
1099 * @return Angle between two polygons in radians.
1100 * @throws IllegalArgumentException Raised if any of the orientation arrays
1101 * do not have length 3.
1102 */
1103 private static double getAngleBetweenOrientations(final double[] orientation1, final double[] orientation2) {
1104 if (orientation1.length != INHOM_COORDS || orientation2.length != INHOM_COORDS) {
1105 throw new IllegalArgumentException();
1106 }
1107
1108 final var x1 = orientation1[0];
1109 final var y1 = orientation1[1];
1110 final var z1 = orientation1[2];
1111
1112 final var x2 = orientation2[0];
1113 final var y2 = orientation2[1];
1114 final var z2 = orientation2[2];
1115
1116 final var norm1 = Math.sqrt(x1 * x1 + y1 * y1 + z1 * z1);
1117 final var norm2 = Math.sqrt(x2 * x2 + y2 * y2 + z2 * z2);
1118
1119 final var dotProduct = (x1 * x2 + y1 * y2 + z1 * z2) / (norm1 * norm2);
1120
1121 return Math.acos(dotProduct);
1122 }
1123 }