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 }