View Javadoc
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 }