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 2D space.
24   */
25  @SuppressWarnings("DuplicatedCode")
26  public class Polygon2D 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       * Default method for triangulation.
46       */
47      public static final TriangulatorMethod DEFAULT_TRIANGULATOR_METHOD = TriangulatorMethod.VAN_GOGH_TRIANGULATOR;
48  
49      /**
50       * List containing vertices of this polygon. Each vertex is a 2D point.
51       */
52      private List<Point2D> vertices;
53  
54      /**
55       * Boolean indicating whether polygon has already been triangulated.
56       */
57      private boolean triangulated;
58  
59      /**
60       * List containing triangles found after triangulating this polygon.
61       * Initially this list will be null until triangulation is done.
62       */
63      private List<Triangle2D> triangles;
64  
65      /**
66       * Method to do triangulation.
67       */
68      private TriangulatorMethod triangulatorMethod;
69  
70      /**
71       * Constructor.
72       *
73       * @param vertices List of vertices forming this polygon.
74       * @throws NotEnoughVerticesException Raised if list does not contain enough
75       *                                    vertices.
76       * @see #MIN_VERTICES
77       */
78      public Polygon2D(final List<Point2D> vertices) throws NotEnoughVerticesException {
79          setVertices(vertices);
80          triangulatorMethod = DEFAULT_TRIANGULATOR_METHOD;
81      }
82  
83      /**
84       * Returns triangulator method. Triangulator method determines the way a
85       * polygon is divided into triangles.
86       * If none has been provided DEFAULT_TRIANGULATOR_METHOD will be returned.
87       *
88       * @return Triangulator method.
89       */
90      public TriangulatorMethod getTriangulatorMethod() {
91          return triangulatorMethod;
92      }
93  
94      /**
95       * Sets triangulator method. A triangulator method determines the way a
96       * polygon is divided into triangles.
97       *
98       * @param triangulatorMethod A triangulator method.
99       */
100     public void setTriangulatorMethod(final TriangulatorMethod triangulatorMethod) {
101         this.triangulatorMethod = triangulatorMethod;
102     }
103 
104     /**
105      * Returns the list of vertices forming this polygon.
106      *
107      * @return List of vertices.
108      */
109     public List<Point2D> getVertices() {
110         return vertices;
111     }
112 
113     /**
114      * Sets list of vertices forming this polygon.
115      *
116      * @param vertices List of vertices.
117      * @throws NotEnoughVerticesException Raised if provided list does not have
118      *                                    enough vertices.
119      * @see #MIN_VERTICES
120      */
121     public final void setVertices(final List<Point2D> vertices) throws NotEnoughVerticesException {
122         if (vertices.size() < MIN_VERTICES) {
123             throw new NotEnoughVerticesException();
124         }
125 
126         if (vertices instanceof Serializable) {
127             this.vertices = vertices;
128         } else {
129             this.vertices = new ArrayList<>(vertices);
130         }
131         triangulated = false;
132         triangles = null;
133     }
134 
135     /**
136      * Determines whether this polygon has already been triangulated.
137      * A polygon will only need to be triangulated once, unless the list of
138      * vertices is reset.
139      *
140      * @return True if polygon has already been triangulated, false otherwise.
141      */
142     public boolean isTriangulated() {
143         return triangulated;
144     }
145 
146     /**
147      * Returns a list of triangles forming this polygon.
148      * This method checks whether this polygon has already been triangulated,
149      * if not, it performs triangulation first.
150      *
151      * @return A list of triangles forming this polygon.
152      * @throws TriangulatorException Raised if triangulation was needed and
153      *                               failed.
154      */
155     public List<Triangle2D> getTriangles() throws TriangulatorException {
156         if (!isTriangulated()) {
157             triangulate();
158         }
159         return triangles;
160     }
161 
162     /**
163      * Returns signed area of this polygon.
164      * The sign of the area determines whether vertices of the polygon are
165      * provided in clockwise (negative sign) or clockwise (positive sign) order.
166      *
167      * @return Signed area of this polygon.
168      */
169     public double getSignedArea() {
170         final var iterator = vertices.iterator();
171 
172         // because there are at least 3
173         // vertices
174         var prevPoint = iterator.next();
175         Point2D curPoint;
176         var signedArea = 0.0;
177 
178         while (iterator.hasNext()) {
179             curPoint = iterator.next();
180 
181             signedArea += prevPoint.getInhomX() * (curPoint.getInhomY() - prevPoint.getInhomY());
182             prevPoint = curPoint;
183         }
184         // on last point, check previous with first
185         curPoint = vertices.get(0);
186         signedArea += prevPoint.getInhomX() * (curPoint.getInhomY() - prevPoint.getInhomY());
187 
188         // signed area is half the sum of cross products of consecutive vertices
189         return signedArea;
190     }
191 
192     /**
193      * Returns area of this polygon.
194      *
195      * @return Area of this polygon.
196      */
197     public double getArea() {
198         return Math.abs(getSignedArea());
199     }
200 
201     /**
202      * Determines whether vertices of this polygon are in clockwise order or
203      * in counterclockwise order.
204      *
205      * @param threshold threshold to determine if vertices are in clockwise
206      *                  order. Usually this value is zero.
207      * @return True if vertices are in clockwise order, false otherwise.
208      */
209     public boolean areVerticesClockwise(final double threshold) {
210         return getSignedArea() < threshold;
211     }
212 
213     /**
214      * Determines whether vertices of this polygon are in clockwise order or in
215      * counterclockwise order.
216      *
217      * @return True if vertices are in clockwise order, false otherwise.
218      */
219     public boolean areVerticesClockwise() {
220         // default threshold to check sign
221         return areVerticesClockwise(0.0);
222     }
223 
224 
225     /**
226      * Returns perimeter of this polygon.
227      * The perimeter is computed as the sum of the distances between consecutive
228      * pairs of vertices.
229      *
230      * @return Perimeter of this polygon.
231      */
232     public double getPerimeter() {
233         // iterate over all vertices and compute their distance
234         final var iterator = vertices.iterator();
235         var prevPoint = iterator.next();
236         Point2D point;
237         var perimeter = 0.0;
238         while (iterator.hasNext()) {
239             point = iterator.next();
240             perimeter += prevPoint.distanceTo(point);
241             prevPoint = point;
242         }
243         // get distance from last point with first one
244         perimeter += prevPoint.distanceTo(vertices.get(0));
245         return perimeter;
246     }
247 
248     /**
249      * Determines if provided point lies within the region defined by this
250      * polygon.
251      * Notice that this method is only ensured to work for polygons having no
252      * holes or crossing borders. It will safely work on any other polygon,
253      * no matter if it is regular, non-regular, convex or concave.
254      *
255      * @param point Point to be checked.
256      * @return True if point lies within the area defined by this polygon, false
257      * otherwise.
258      * @throws TriangulatorException Raised if triangulation was required but
259      *                               failed.
260      */
261     public boolean isInside(final Point2D point) throws TriangulatorException {
262         return isInside(point, DEFAULT_THRESHOLD);
263     }
264 
265     /**
266      * Determines if provided point lies within the region defined by this
267      * polygon.
268      * Notice that this method is only ensured to work for polygons having no
269      * holes or crossing borders. It will safely work on any other polygon,
270      * no matter if it is regular, non-regular, convex or concave.
271      *
272      * @param point     Point to be checked.
273      * @param threshold Threshold to determine whether point lies inside this
274      *                  polygon. Usually this value should be small.
275      * @return True if point lies within the area defined by this polygon, false
276      * otherwise.
277      * @throws IllegalArgumentException Raised if provided threshold is negative.
278      * @throws TriangulatorException    Raised if triangulation was required but
279      *                                  failed.
280      */
281     public boolean isInside(final Point2D point, final double threshold) 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 Point2D getCenter() {
302         final var result = Point2D.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 Point2D result) {
315         // compute average location of all vertices
316         var inhomX = 0.0;
317         var inhomY = 0.0;
318         final var total = vertices.size();
319 
320         for (final var point : vertices) {
321             inhomX += point.getInhomX() / total;
322             inhomY += point.getInhomY() / total;
323         }
324         result.setInhomogeneousCoordinates(inhomX, inhomY);
325     }
326 
327     /**
328      * Determines whether provided point is locus of the borders defined by
329      * the vertices of this polygon. A point will be locus if it lies in the
330      * line defined by two consecutive vertices up to a certain threshold of
331      * error.
332      *
333      * @param point     Point to be checked.
334      * @param threshold Threshold of allowed error. This should usually be a
335      *                  small value.
336      * @return True if provided point lies in a border of this polygon, false
337      * otherwise.
338      * @throws IllegalArgumentException Raised if provided threshold is negative.
339      */
340     public boolean isLocus(final Point2D point, final double threshold) {
341         if (threshold < MIN_THRESHOLD) {
342             throw new IllegalArgumentException();
343         }
344 
345         final var iterator = vertices.iterator();
346         // it's ok because there are at
347         // least 3 vertices
348         var prevPoint = iterator.next();
349         Point2D curPoint;
350         while (iterator.hasNext()) {
351             curPoint = iterator.next();
352             if (point.isBetween(prevPoint, curPoint, threshold)) {
353                 return true;
354             }
355             prevPoint = curPoint;
356         }
357 
358         // check last point with first
359         return point.isBetween(prevPoint, vertices.get(0), threshold);
360     }
361 
362     /**
363      * Determines whether provided point is locus of the borders defined by the
364      * vertices of this polygon. A point will be locus if it lies in the line
365      * defined by two consecutive vertices.
366      *
367      * @param point Point to be checked.
368      * @return True if provided point lies in a border of this polygon, false
369      * otherwise.
370      */
371     public boolean isLocus(final Point2D point) {
372         return isLocus(point, DEFAULT_THRESHOLD);
373     }
374 
375     /**
376      * Returns the shortest distance from provided point to a border of this
377      * polygon. Note that borders are segments defined by consecutive vertices.
378      *
379      * @param point Point to be checked.
380      * @return Shortest distance from provided point to this polygon.
381      */
382     public double getShortestDistance(final Point2D point) {
383         // iterate over all vertices and compute their distance
384         var iterator = vertices.iterator();
385         var prevPoint = iterator.next();
386         // to increase accuracy
387         prevPoint.normalize();
388         Point2D curPoint;
389         var bestDist = Double.MAX_VALUE;
390         double dist;
391         var found = false;
392         final var line = new Line2D();
393         final var pointInLine = Point2D.create();
394 
395         while (iterator.hasNext()) {
396             curPoint = iterator.next();
397             // to increase accuracy
398             curPoint.normalize();
399 
400             // check if point lies in the segment of the boundary of this polygon
401             if (point.isBetween(curPoint, prevPoint)) {
402                 return 0.0;
403             }
404 
405             line.setParametersFromPairOfPoints(curPoint, prevPoint);
406             // to increase accuracy
407             line.normalize();
408 
409             // find the closest point to line
410             line.closestPoint(point, pointInLine);
411             // to increase accuracy
412             pointInLine.normalize();
413 
414             if (pointInLine.isBetween(curPoint, prevPoint)) {
415                 // closest point lies within segment of polygon boundary, so we
416                 // keep distance
417                 dist = point.distanceTo(pointInLine);
418                 if (dist < bestDist) {
419                     // a better point has been found
420                     bestDist = dist;
421                     found = true;
422                 }
423             }
424 
425             prevPoint = curPoint;
426         }
427 
428         // try last vertex with first
429         // check if point lies in the segment of the boundary of this polygon
430         final var first = vertices.get(0);
431         if (point.isBetween(prevPoint, first)) {
432             return 0.0;
433         }
434 
435         line.setParametersFromPairOfPoints(prevPoint, first);
436         // to increase accuracy
437         line.normalize();
438 
439         // find the closest point to line
440         line.closestPoint(point, pointInLine);
441         // to increase accuracy
442         pointInLine.normalize();
443 
444         if (pointInLine.isBetween(prevPoint, first)) {
445             // closest point lies within segment of polygon boundary, so we
446             // keep distance
447             dist = point.distanceTo(pointInLine);
448             if (dist < bestDist) {
449                 // a better point has been found
450                 bestDist = dist;
451                 found = true;
452             }
453         }
454 
455         if (!found) {
456             // no closest point was found on a segment belonging to polygon
457             // boundary, so we search for the closest vertex
458             iterator = vertices.iterator();
459             while (iterator.hasNext()) {
460                 // a better vertex has been found
461                 curPoint = iterator.next();
462                 dist = point.distanceTo(curPoint);
463                 if (dist < bestDist) {
464                     bestDist = dist;
465                 }
466             }
467         }
468 
469         return bestDist;
470     }
471 
472     /**
473      * Returns the closest point to provided point that is locus of this
474      * polygon (i.e. lies on a border of this polygon).
475      *
476      * @param point Point to be checked.
477      * @return Closest point being locus of this polygon.
478      */
479     public Point2D getClosestPoint(final Point2D point) {
480         final var result = Point2D.create();
481         closestPoint(point, result);
482         return result;
483     }
484 
485     /**
486      * Computes the closes point to provided point that is locus of this
487      * polygon (i.e. lies on a border of this polygon).
488      *
489      * @param point  Point to be checked.
490      * @param result Instance where the closest point will be stored.
491      */
492     public void closestPoint(final Point2D point, final Point2D result) {
493         // iterate over all vertices and compute their distance
494         var iterator = vertices.iterator();
495         var prevPoint = iterator.next();
496         // to increase accuracy
497         prevPoint.normalize();
498 
499         Point2D curPoint;
500         var bestDist = Double.MAX_VALUE;
501         double dist;
502         var found = false;
503         final var line = new Line2D();
504         final var pointInLine = Point2D.create();
505 
506         while (iterator.hasNext()) {
507             curPoint = iterator.next();
508             // to increase accuracy
509             curPoint.normalize();
510 
511             // check if point lies in the segment of the boundary of this polygon
512             if (point.isBetween(curPoint, prevPoint)) {
513                 result.setCoordinates(point);
514                 return;
515             }
516 
517             line.setParametersFromPairOfPoints(curPoint, prevPoint);
518             // to increase accuracy
519             line.normalize();
520 
521             // find the closest point to line
522             line.closestPoint(point, pointInLine);
523             // to increase accuracy
524             pointInLine.normalize();
525 
526             if (pointInLine.isBetween(curPoint, prevPoint)) {
527                 // closest point lies within segment of polygon boundary, so we
528                 // keep distance and point
529                 dist = point.distanceTo(pointInLine);
530                 if (dist < bestDist) {
531                     // a better point has been found
532                     bestDist = dist;
533                     result.setCoordinates(pointInLine);
534                     found = true;
535                 }
536             }
537 
538             prevPoint = curPoint;
539         }
540 
541         // try last vertex with first
542         // check if point lies in the segment of the boundary of this polygon
543         final var first = vertices.get(0);
544         if (point.isBetween(prevPoint, first)) {
545             result.setCoordinates(point);
546             return;
547         }
548 
549         line.setParametersFromPairOfPoints(prevPoint, first);
550         // to increase accuracy
551         line.normalize();
552 
553         // find the closest point to line
554         line.closestPoint(point, pointInLine);
555         // to increase accuracy
556         pointInLine.normalize();
557 
558         if (pointInLine.isBetween(prevPoint, first)) {
559             // closest point lies within segment of polygon boundary, so we
560             // keep distance
561             dist = point.distanceTo(pointInLine);
562             if (dist < bestDist) {
563                 // a better point has been found
564                 bestDist = dist;
565                 result.setCoordinates(pointInLine);
566                 found = true;
567             }
568         }
569 
570         if (!found) {
571             // no closest point was found on a segment belonging to polygon
572             // boundary, so we search for the closest vertex
573             iterator = vertices.iterator();
574             while (iterator.hasNext()) {
575                 curPoint = iterator.next();
576                 dist = point.distanceTo(curPoint);
577                 if (dist < bestDist) {
578                     // a better vertex has been found
579                     bestDist = dist;
580                     result.setCoordinates(curPoint);
581                 }
582             }
583         }
584     }
585 
586     /**
587      * Triangulates this polygon using this polygon's triangulator method.
588      * A polygon only will be triangulated once when required or this method is
589      * called.
590      * This method will make no action if a polygon is already triangulated
591      * unless it's vertices are reset.
592      *
593      * @throws TriangulatorException Raised if triangulation failed
594      * @see #getTriangulatorMethod
595      * @see #setTriangulatorMethod(TriangulatorMethod)
596      */
597     public void triangulate() throws TriangulatorException {
598         if (!triangulated) {
599             final var triangulator = Triangulator2D.create(triangulatorMethod);
600             triangles = triangulator.triangulate(vertices);
601             triangulated = true;
602         }
603     }
604 }