1 | package org.expeditee.core.bounds;
|
---|
2 |
|
---|
3 | import java.util.LinkedList;
|
---|
4 | import java.util.List;
|
---|
5 |
|
---|
6 | import org.expeditee.core.Line;
|
---|
7 | import org.expeditee.core.Point;
|
---|
8 |
|
---|
9 | /**
|
---|
10 | * A bounds that is defined as the area enclosed by a polygon formed
|
---|
11 | * from a set of points. When considering if something is enclosed by
|
---|
12 | * a polygon, the set of points is always considered closed (even if
|
---|
13 | * the first and last points arenn't equal).
|
---|
14 | *
|
---|
15 | * @author cts16
|
---|
16 | */
|
---|
17 | public class PolygonBounds extends Bounds {
|
---|
18 |
|
---|
19 | /** The points that form this polygon. */
|
---|
20 | private List<Point> _points;
|
---|
21 |
|
---|
22 | /** The right-most extent of this bounds. */
|
---|
23 | Integer _maxX;
|
---|
24 | /** The left-most extent of this bounds. */
|
---|
25 | Integer _minX;
|
---|
26 | /** The down-most extent of this bounds. */
|
---|
27 | Integer _maxY;
|
---|
28 | /** The up-most extent of this bounds. */
|
---|
29 | Integer _minY;
|
---|
30 |
|
---|
31 | /** Standard constructor (creates a degenerate polygon with no points). */
|
---|
32 | public PolygonBounds()
|
---|
33 | {
|
---|
34 | _points = new LinkedList<Point>();
|
---|
35 | _maxX = _minX = _maxY = _minY = null;
|
---|
36 | }
|
---|
37 |
|
---|
38 | /** Copy constructor. */
|
---|
39 | public PolygonBounds(PolygonBounds other)
|
---|
40 | {
|
---|
41 | this();
|
---|
42 | for (Point p : other._points) {
|
---|
43 | addPoint(p);
|
---|
44 | }
|
---|
45 | }
|
---|
46 |
|
---|
47 | /**
|
---|
48 | * Constructs a polygon from a set of lines. Lines should be connected
|
---|
49 | * end to end, otherwise the resulting polygon may not match the given input.
|
---|
50 | */
|
---|
51 | public PolygonBounds(Line[] lines)
|
---|
52 | {
|
---|
53 | this();
|
---|
54 | for (Line line : lines) {
|
---|
55 | addPoint(line.getFirstEnd());
|
---|
56 | }
|
---|
57 | addPoint(lines[lines.length - 1].getSecondEnd());
|
---|
58 | }
|
---|
59 |
|
---|
60 | /** Adds a new point to the polygon. */
|
---|
61 | public void addPoint(Point p)
|
---|
62 | {
|
---|
63 | addPoint(p.getX(), p.getY());
|
---|
64 | }
|
---|
65 |
|
---|
66 | /** Adds a new point to the polygon. */
|
---|
67 | public void addPoint(int x, int y)
|
---|
68 | {
|
---|
69 | _points.add(new Point(x, y));
|
---|
70 | if (_maxX == null || x > _maxX.intValue()) _maxX = new Integer(x);
|
---|
71 | if (_minX == null || x < _minX.intValue()) _minX = new Integer(x);
|
---|
72 | if (_maxY == null || y > _maxY.intValue()) _maxY = new Integer(y);
|
---|
73 | if (_minY == null || y < _minY.intValue()) _minY = new Integer(y);
|
---|
74 | }
|
---|
75 |
|
---|
76 | /** Gets the point at the given index in this polygon. */
|
---|
77 | public Point getPoint(int index)
|
---|
78 | {
|
---|
79 | if (0 <= index && index < getPointCount()) return _points.get(index).clone();
|
---|
80 |
|
---|
81 | return null;
|
---|
82 | }
|
---|
83 |
|
---|
84 | /** Returns the number of points in this polygon. */
|
---|
85 | public int getPointCount()
|
---|
86 | {
|
---|
87 | return _points.size();
|
---|
88 | }
|
---|
89 |
|
---|
90 | /** Returns a copy of the points of this polygon as an array. */
|
---|
91 | public Point[] toArray()
|
---|
92 | {
|
---|
93 | Point[] ret = new Point[_points.size()];
|
---|
94 |
|
---|
95 | for (int i = 0; i < _points.size(); i++) {
|
---|
96 | ret[i] = _points.get(i).clone();
|
---|
97 | }
|
---|
98 |
|
---|
99 | return ret;
|
---|
100 | }
|
---|
101 |
|
---|
102 | @Override
|
---|
103 | public PolygonBounds translate(int x, int y)
|
---|
104 | {
|
---|
105 | for (Point p : _points) p.add(x, y);
|
---|
106 |
|
---|
107 | if (_maxX != null) _maxX = new Integer(_maxX.intValue() + x);
|
---|
108 | if (_minX != null) _minX = new Integer(_minX.intValue() + x);
|
---|
109 | if (_maxY != null) _maxY = new Integer(_maxY.intValue() + y);
|
---|
110 | if (_minY != null) _minY = new Integer(_minY.intValue() + y);
|
---|
111 |
|
---|
112 | return this;
|
---|
113 | }
|
---|
114 |
|
---|
115 | /**
|
---|
116 | * Rotates all of the points in this polygon about the given x/y coordinate.
|
---|
117 | *
|
---|
118 | * @param angle Clockwise angle of rotation in radians.
|
---|
119 | */
|
---|
120 | public PolygonBounds rotate(double angle, int x, int y)
|
---|
121 | {
|
---|
122 | // Rotate each point
|
---|
123 | for (Point p : _points) {
|
---|
124 | p.rotate(angle, x, y);
|
---|
125 | }
|
---|
126 |
|
---|
127 | // Non-trivial change to mins/maxs, so recalculate
|
---|
128 | recalculateExtents();
|
---|
129 |
|
---|
130 | return this;
|
---|
131 | }
|
---|
132 |
|
---|
133 | /**
|
---|
134 | * Rotates the polygon around a point.
|
---|
135 | *
|
---|
136 | * @param angle
|
---|
137 | * The clockwise angle to rotate the polygon through (in radians).
|
---|
138 | *
|
---|
139 | * @param cor
|
---|
140 | * The centre of rotation.
|
---|
141 | */
|
---|
142 | public void rotate(double angle, Point cor)
|
---|
143 | {
|
---|
144 | rotate(angle, cor.getX(), cor.getY());
|
---|
145 | }
|
---|
146 |
|
---|
147 | /**
|
---|
148 | * Code adapted from java.awt.Polygon.contains(double x, double y).
|
---|
149 | *
|
---|
150 | * @author cts16
|
---|
151 | */
|
---|
152 | @Override
|
---|
153 | public boolean contains(int x, int y) {
|
---|
154 | int npoints = _points.size();
|
---|
155 | if (npoints <= 2) {
|
---|
156 | return false;
|
---|
157 | }
|
---|
158 |
|
---|
159 | int hits = 0;
|
---|
160 |
|
---|
161 | int lastx = _points.get(npoints - 1).getX();
|
---|
162 | int lasty = _points.get(npoints - 1).getY();
|
---|
163 | int curx, cury;
|
---|
164 |
|
---|
165 | // Walk the edges of the polygon
|
---|
166 | for (int i = 0; i < npoints; lastx = curx, lasty = cury, i++) {
|
---|
167 | curx = _points.get(i).getX();
|
---|
168 | cury = _points.get(i).getY();
|
---|
169 |
|
---|
170 | if (cury == lasty) {
|
---|
171 | continue;
|
---|
172 | }
|
---|
173 |
|
---|
174 | int leftx;
|
---|
175 | if (curx < lastx) {
|
---|
176 | if (x >= lastx) {
|
---|
177 | continue;
|
---|
178 | }
|
---|
179 | leftx = curx;
|
---|
180 | } else {
|
---|
181 | if (x >= curx) {
|
---|
182 | continue;
|
---|
183 | }
|
---|
184 | leftx = lastx;
|
---|
185 | }
|
---|
186 |
|
---|
187 | double test1, test2;
|
---|
188 | if (cury < lasty) {
|
---|
189 | if (y < cury || y >= lasty) {
|
---|
190 | continue;
|
---|
191 | }
|
---|
192 | if (x < leftx) {
|
---|
193 | hits++;
|
---|
194 | continue;
|
---|
195 | }
|
---|
196 | test1 = x - curx;
|
---|
197 | test2 = y - cury;
|
---|
198 | } else {
|
---|
199 | if (y < lasty || y >= cury) {
|
---|
200 | continue;
|
---|
201 | }
|
---|
202 | if (x < leftx) {
|
---|
203 | hits++;
|
---|
204 | continue;
|
---|
205 | }
|
---|
206 | test1 = x - lastx;
|
---|
207 | test2 = y - lasty;
|
---|
208 | }
|
---|
209 |
|
---|
210 | if (test1 < (test2 / (lasty - cury) * (lastx - curx))) {
|
---|
211 | hits++;
|
---|
212 | }
|
---|
213 | }
|
---|
214 |
|
---|
215 | return ((hits & 1) != 0);
|
---|
216 | }
|
---|
217 |
|
---|
218 | @Override
|
---|
219 | public int getMaxX() {
|
---|
220 | return _maxX != null ? _maxX : 0;
|
---|
221 | }
|
---|
222 |
|
---|
223 | @Override
|
---|
224 | public int getMinX() {
|
---|
225 | return _minX != null ? _minX : 0;
|
---|
226 | }
|
---|
227 |
|
---|
228 | @Override
|
---|
229 | public int getMaxY() {
|
---|
230 | return _maxY != null ? _maxY : 0;
|
---|
231 | }
|
---|
232 |
|
---|
233 | @Override
|
---|
234 | public int getMinY() {
|
---|
235 | return _minY != null ? _minY : 0;
|
---|
236 | }
|
---|
237 |
|
---|
238 | @Override
|
---|
239 | public boolean equals(Bounds other) {
|
---|
240 | if (other instanceof PolygonBounds) {
|
---|
241 | PolygonBounds p = (PolygonBounds) other;
|
---|
242 | if (p._points.size() == this._points.size()) {
|
---|
243 | for (int i = 0; i < p._points.size(); i++) {
|
---|
244 | if (!(p._points.get(i).equals(this._points.get(i)))) return false;
|
---|
245 | }
|
---|
246 | return true;
|
---|
247 | }
|
---|
248 | }
|
---|
249 |
|
---|
250 | return false;
|
---|
251 | }
|
---|
252 |
|
---|
253 | /** Convenience method to create a polygon in the shape of a diamond. */
|
---|
254 | public static PolygonBounds getDiamond(int width, int height)
|
---|
255 | {
|
---|
256 | PolygonBounds diamond = new PolygonBounds();
|
---|
257 |
|
---|
258 | diamond.addPoint(0, -height / 2);
|
---|
259 | diamond.addPoint(width / 2, 0);
|
---|
260 | diamond.addPoint(0, height / 2);
|
---|
261 | diamond.addPoint(-width / 2, 0);
|
---|
262 |
|
---|
263 | return diamond;
|
---|
264 | }
|
---|
265 |
|
---|
266 | /** Convenience method to create a polygon in the shape of a triangle. */
|
---|
267 | public static PolygonBounds getTriangle(int base, int height)
|
---|
268 | {
|
---|
269 | PolygonBounds triangle = new PolygonBounds();
|
---|
270 |
|
---|
271 | triangle.addPoint(0, -height / 2);
|
---|
272 | triangle.addPoint(base / 2, height / 2);
|
---|
273 | triangle.addPoint(-base / 2, height / 2);
|
---|
274 |
|
---|
275 | return triangle;
|
---|
276 | }
|
---|
277 |
|
---|
278 | @Override
|
---|
279 | public boolean intersects(AxisAlignedBoxBounds other) {
|
---|
280 | // Check if any polygon points are inside the box
|
---|
281 | for (Point p : _points) {
|
---|
282 | if (other.contains(p)) return true;
|
---|
283 | }
|
---|
284 |
|
---|
285 | // Check if any of the border lines intersect
|
---|
286 | Line[] boxLines = other.getBorderLines();
|
---|
287 | Line[] polyLines = getBorderLines();
|
---|
288 | if (Line.anyIntersections(boxLines, polyLines, Line.PIXEL_ERROR)) return true;
|
---|
289 |
|
---|
290 | // Check if any of the box points are inside the polygon
|
---|
291 | // (only test p1 as p2 is the p1 of another line)
|
---|
292 | for (Line boxLine : boxLines) {
|
---|
293 | if (contains(boxLine.getFirstEnd())) return true;
|
---|
294 | }
|
---|
295 |
|
---|
296 | // If all tests fail, no intersection
|
---|
297 | return false;
|
---|
298 | }
|
---|
299 |
|
---|
300 | @Override
|
---|
301 | public boolean intersects(CombinationBounds other) {
|
---|
302 | // Defer to the CombinationBounds routine
|
---|
303 | return other.intersects(this);
|
---|
304 | }
|
---|
305 |
|
---|
306 | @Override
|
---|
307 | public boolean intersects(EllipticalBounds other) {
|
---|
308 | // Defer to the EllipticalBounds routine
|
---|
309 | return other.intersects(this);
|
---|
310 | }
|
---|
311 |
|
---|
312 | @Override
|
---|
313 | public boolean intersects(PolygonBounds other) {
|
---|
314 | // Check if any of the border lines intersect
|
---|
315 | Line[] otherLines = other.getBorderLines();
|
---|
316 | Line[] thisLines = getBorderLines();
|
---|
317 | if (Line.anyIntersections(otherLines, thisLines, Line.PIXEL_ERROR)) return true;
|
---|
318 |
|
---|
319 | // If no line intersections, one polygon must be inside the other to intersect
|
---|
320 | return other.contains(thisLines[0].getFirstEnd()) || this.contains(otherLines[0].getFirstEnd());
|
---|
321 | }
|
---|
322 |
|
---|
323 | /** Gets an array of the vertices that make up this polygon. */
|
---|
324 | public Point[] getVertices()
|
---|
325 | {
|
---|
326 | Point[] ret = new Point[_points.size()];
|
---|
327 |
|
---|
328 | for (int i = 0; i < _points.size(); i++) {
|
---|
329 | ret[i] = _points.get(i).clone();
|
---|
330 | }
|
---|
331 |
|
---|
332 | return ret;
|
---|
333 | }
|
---|
334 |
|
---|
335 | /** Gets an array of lines that represent the (closed) border of this polygon. */
|
---|
336 | public Line[] getBorderLines()
|
---|
337 | {
|
---|
338 | return getBorderLines(true);
|
---|
339 | }
|
---|
340 |
|
---|
341 | /**
|
---|
342 | * Gets an array of lines that represent the border of this polygon.
|
---|
343 | *
|
---|
344 | * @param autoClose
|
---|
345 | * Whether to automatically create a line from the last point to the
|
---|
346 | * first to close this polygon (ignored if first and last point are equal).
|
---|
347 | *
|
---|
348 | * @return
|
---|
349 | * The array of border lines.
|
---|
350 | */
|
---|
351 | public Line[] getBorderLines(boolean autoClose)
|
---|
352 | {
|
---|
353 | // Don't need to autoClose if first and last points are equal
|
---|
354 | autoClose = autoClose && _points.get(0).equals(_points.get(_points.size() - 1));
|
---|
355 |
|
---|
356 | // Calculate how many lines we will need to generate
|
---|
357 | int nLines = _points.size();
|
---|
358 | if (!autoClose) nLines--;
|
---|
359 |
|
---|
360 | // Generate the lines
|
---|
361 | Line[] lines = new Line[nLines];
|
---|
362 | for (int i = 0; i < nLines; i++) {
|
---|
363 | Point p1 = new Point(_points.get(i));
|
---|
364 | Point p2 = new Point(_points.get((i + 1) % _points.size()));
|
---|
365 | lines[i] = new Line(p1, p2);
|
---|
366 | }
|
---|
367 |
|
---|
368 | return lines;
|
---|
369 | }
|
---|
370 |
|
---|
371 | @Override
|
---|
372 | public double getArea()
|
---|
373 | {
|
---|
374 | if (_points.size() < 3) return 0.0;
|
---|
375 |
|
---|
376 | double area = 0.0;
|
---|
377 |
|
---|
378 | for (int i = 0; i < _points.size(); i++) {
|
---|
379 | Point p1 = _points.get(i);
|
---|
380 | Point p2 = _points.get((i + 1) % _points.size());
|
---|
381 | area += p1.getX() * p2.getY() - p2.getX() * p1.getY();
|
---|
382 | }
|
---|
383 |
|
---|
384 | return Math.abs(area / 2);
|
---|
385 | }
|
---|
386 |
|
---|
387 | @Override
|
---|
388 | public boolean perimeterContains(Point p, double error) {
|
---|
389 | Line[] lines = getBorderLines();
|
---|
390 |
|
---|
391 | for (Line line : lines) {
|
---|
392 | if (line.contains(p, error)) return true;
|
---|
393 | }
|
---|
394 |
|
---|
395 | return false;
|
---|
396 | }
|
---|
397 |
|
---|
398 | @Override
|
---|
399 | public boolean completelyContains(AxisAlignedBoxBounds other)
|
---|
400 | {
|
---|
401 | // Treat the box as a polygon
|
---|
402 | return completelyContains(other.getPolygon());
|
---|
403 | }
|
---|
404 |
|
---|
405 | @Override
|
---|
406 | public boolean completelyContains(CombinationBounds other)
|
---|
407 | {
|
---|
408 | // Can't contain nothing
|
---|
409 | if (other == null) return false;
|
---|
410 |
|
---|
411 | // Must contain all sub-bounds of the combination bounds
|
---|
412 | for (Bounds bounds : other._bounds) {
|
---|
413 | if (!completelyContains(bounds)) return false;
|
---|
414 | }
|
---|
415 |
|
---|
416 | return true;
|
---|
417 | }
|
---|
418 |
|
---|
419 | @Override
|
---|
420 | public boolean completelyContains(EllipticalBounds other)
|
---|
421 | {
|
---|
422 | // Can't contain nothing
|
---|
423 | if (other == null) return false;
|
---|
424 |
|
---|
425 | // No polygon vertices can be inside the ellipse
|
---|
426 | for (Point vertex : _points) {
|
---|
427 | if (other.contains(vertex)) return false;
|
---|
428 | }
|
---|
429 |
|
---|
430 | // No boundary lines can intersect the ellipse
|
---|
431 | for (Line line : getBorderLines()) {
|
---|
432 | if (other.isIntersectedByLine(line)) return false;
|
---|
433 | }
|
---|
434 |
|
---|
435 | return true;
|
---|
436 | }
|
---|
437 |
|
---|
438 | @Override
|
---|
439 | public boolean completelyContains(PolygonBounds other)
|
---|
440 | {
|
---|
441 | // Can't contain nothing
|
---|
442 | if (other == null) return false;
|
---|
443 |
|
---|
444 | // Must contain all points of other
|
---|
445 | for (Point p : other._points) {
|
---|
446 | if (!contains(p)) return false;
|
---|
447 | }
|
---|
448 |
|
---|
449 | // Can't be any line intersections
|
---|
450 | Line[] thisLines = getBorderLines();
|
---|
451 | Line[] otherLines = other.getBorderLines();
|
---|
452 | if (Line.anyIntersections(thisLines, otherLines, Line.PIXEL_ERROR)) return false;
|
---|
453 |
|
---|
454 | return true;
|
---|
455 | }
|
---|
456 |
|
---|
457 | /** Whether the given point is a vertex of this polygon. */
|
---|
458 | public boolean isVertex(Point p)
|
---|
459 | {
|
---|
460 | return _points.contains(p);
|
---|
461 | }
|
---|
462 |
|
---|
463 | /** Whether or not this polygon is closed (first and last points are equal). */
|
---|
464 | public boolean isClosed()
|
---|
465 | {
|
---|
466 | return _points.get(0).equals(_points.get(getPointCount() - 1));
|
---|
467 | }
|
---|
468 |
|
---|
469 | /** Closes this polygon (adds the first point as the last). */
|
---|
470 | public PolygonBounds close()
|
---|
471 | {
|
---|
472 | if (!isClosed()) addPoint(_points.get(0).clone());
|
---|
473 |
|
---|
474 | return this;
|
---|
475 | }
|
---|
476 |
|
---|
477 | /** Creates a polygon that represents the same area as a box bounds. */
|
---|
478 | public static PolygonBounds fromBox(AxisAlignedBoxBounds box)
|
---|
479 | {
|
---|
480 | if (box == null) return null;
|
---|
481 |
|
---|
482 | // Get the corners of the box
|
---|
483 | Point[] corners = box.getCorners();
|
---|
484 |
|
---|
485 | // Add the corners to a new polygon
|
---|
486 | PolygonBounds ret = new PolygonBounds();
|
---|
487 | for (Point corner : corners) ret.addPoint(corner);
|
---|
488 |
|
---|
489 | return ret.close();
|
---|
490 | }
|
---|
491 |
|
---|
492 | /**
|
---|
493 | * Creates a polygon that approximates the given ellipse.
|
---|
494 | *
|
---|
495 | * @param nSegments
|
---|
496 | * The number of segments the resulting polygon should have. Must be at least 3.
|
---|
497 | */
|
---|
498 | public static PolygonBounds fromEllipse(EllipticalBounds ellipse, int nSegments)
|
---|
499 | {
|
---|
500 | if (ellipse == null || nSegments < 3) return null;
|
---|
501 |
|
---|
502 | int centreX = ellipse.getCentre().getX();
|
---|
503 | int centreY = ellipse.getCentre().getY();
|
---|
504 | double xRadius = ellipse.getDiameters().width / 2.0;
|
---|
505 | double yRadius = ellipse.getDiameters().height / 2.0;
|
---|
506 | double radians = 0.0;
|
---|
507 |
|
---|
508 | PolygonBounds ret = new PolygonBounds();
|
---|
509 | for (int i = 0; i < nSegments; i++) {
|
---|
510 | int x = (int) (xRadius * Math.cos(radians)) + centreX;
|
---|
511 | int y = (int) (yRadius * Math.sin(radians)) + centreY;
|
---|
512 | ret.addPoint(x, y);
|
---|
513 | radians += (2.0 * Math.PI) / nSegments;
|
---|
514 | }
|
---|
515 |
|
---|
516 | return ret.close();
|
---|
517 | }
|
---|
518 |
|
---|
519 | @Override
|
---|
520 | public PolygonBounds clone()
|
---|
521 | {
|
---|
522 | return new PolygonBounds(this);
|
---|
523 | }
|
---|
524 |
|
---|
525 | /**
|
---|
526 | * Recalculates the min/max x/y coordinates. Normally this should be done incrementally
|
---|
527 | * as points are added, but sometimes you just need to completely redo them.
|
---|
528 | */
|
---|
529 | private void recalculateExtents()
|
---|
530 | {
|
---|
531 | _maxX = _minX = _maxY = _minY = null;
|
---|
532 |
|
---|
533 | for (Point p : _points) {
|
---|
534 | if (_maxX == null || p.getX() > _maxX.intValue()) _maxX = new Integer(p.getX());
|
---|
535 | if (_minX == null || p.getX() < _minX.intValue()) _minX = new Integer(p.getX());
|
---|
536 | if (_maxY == null || p.getY() > _maxY.intValue()) _maxY = new Integer(p.getY());
|
---|
537 | if (_minY == null || p.getY() < _minY.intValue()) _minY = new Integer(p.getY());
|
---|
538 | }
|
---|
539 | }
|
---|
540 |
|
---|
541 | public String toString() {
|
---|
542 | StringBuilder sb = new StringBuilder("PolygonBounds with: ");
|
---|
543 | sb.append("[MinX: " + getMinX() + "]");
|
---|
544 | sb.append("[MinY: " + getMinY() + "]");
|
---|
545 | sb.append("[MaxX: " + getMaxX() + "]");
|
---|
546 | sb.append("[MaxY: " + getMaxY() + "]");
|
---|
547 | return sb.toString();
|
---|
548 | }
|
---|
549 |
|
---|
550 | }
|
---|