001 /* ======================================================================== 002 * JCommon : a free general purpose class library for the Java(tm) platform 003 * ======================================================================== 004 * 005 * (C) Copyright 2000-2005, by Object Refinery Limited and Contributors. 006 * 007 * Project Info: http://www.jfree.org/jcommon/index.html 008 * 009 * This library is free software; you can redistribute it and/or modify it 010 * under the terms of the GNU Lesser General Public License as published by 011 * the Free Software Foundation; either version 2.1 of the License, or 012 * (at your option) any later version. 013 * 014 * This library is distributed in the hope that it will be useful, but 015 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 016 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public 017 * License for more details. 018 * 019 * You should have received a copy of the GNU Lesser General Public 020 * License along with this library; if not, write to the Free Software 021 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, 022 * USA. 023 * 024 * [Java is a trademark or registered trademark of Sun Microsystems, Inc. 025 * in the United States and other countries.] 026 * 027 * ------------------- 028 * ShapeUtilities.java 029 * ------------------- 030 * (C)opyright 2003-2005, by Object Refinery Limited and Contributors. 031 * 032 * Original Author: David Gilbert (for Object Refinery Limited); 033 * Contributor(s): -; 034 * 035 * $Id: ShapeUtilities.java,v 1.17 2005/11/02 16:31:50 mungady Exp $ 036 * 037 * Changes 038 * ------- 039 * 13-Aug-2003 : Version 1 (DG); 040 * 16-Mar-2004 : Moved rotateShape() from RefineryUtilities.java to here (DG); 041 * 13-May-2004 : Added new shape creation methods (DG); 042 * 30-Sep-2004 : Added createLineRegion() method (DG); 043 * Moved drawRotatedShape() method from RefineryUtilities class 044 * to this class (DG); 045 * 04-Oct-2004 : Renamed ShapeUtils --> ShapeUtilities (DG); 046 * 26-Oct-2004 : Added a method to test the equality of two Line2D 047 * instances (DG); 048 * 10-Nov-2004 : Added new translateShape() and equal(Ellipse2D, Ellipse2D) 049 * methods (DG); 050 * 11-Nov-2004 : Renamed translateShape() --> createTranslatedShape() (DG); 051 * 07-Jan-2005 : Minor Javadoc fix (DG); 052 * 11-Jan-2005 : Removed deprecated code in preparation for 1.0.0 release (DG); 053 * 21-Jan-2005 : Modified return type of RectangleAnchor.coordinates() 054 * method (DG); 055 * 22-Feb-2005 : Added equality tests for Arc2D and GeneralPath (DG); 056 * 16-Mar-2005 : Fixed bug where equal(Shape, Shape) fails for two Polygon 057 * instances (DG); 058 * 059 */ 060 061 package org.jfree.util; 062 063 import java.awt.Graphics2D; 064 import java.awt.Polygon; 065 import java.awt.Shape; 066 import java.awt.geom.AffineTransform; 067 import java.awt.geom.Arc2D; 068 import java.awt.geom.Ellipse2D; 069 import java.awt.geom.GeneralPath; 070 import java.awt.geom.Line2D; 071 import java.awt.geom.PathIterator; 072 import java.awt.geom.Point2D; 073 import java.awt.geom.Rectangle2D; 074 import java.util.Arrays; 075 076 import org.jfree.ui.RectangleAnchor; 077 078 /** 079 * Utility methods for {@link Shape} objects. 080 * 081 * @author David Gilbert 082 */ 083 public class ShapeUtilities { 084 085 /** 086 * Prevents instantiation. 087 */ 088 private ShapeUtilities() { 089 } 090 091 /** 092 * Returns a clone of the specified shape, or <code>null</code>. At the 093 * current time, this method supports cloning for instances of 094 * <code>Line2D</code>, <code>RectangularShape</code>, <code>Area</code> 095 * and <code>GeneralPath</code>. 096 * <p> 097 * <code>RectangularShape</code> includes <code>Arc2D</code>, 098 * <code>Ellipse2D</code>, <code>Rectangle2D</code>, 099 * <code>RoundRectangle2D</code>. 100 * 101 * @param shape the shape to clone (<code>null</code> permitted, 102 * returns <code>null</code>). 103 * 104 * @return A clone or <code>null</code>. 105 */ 106 public static Shape clone(final Shape shape) { 107 108 if (shape instanceof Cloneable) { 109 try { 110 return (Shape) ObjectUtilities.clone(shape); 111 } 112 catch (CloneNotSupportedException cnse) { 113 } 114 } 115 final Shape result = null; 116 return result; 117 } 118 119 /** 120 * Tests two shapes for equality. If both shapes are <code>null</code>, 121 * this method will return <code>true</code>. 122 * <p> 123 * In the current implementation, the following shapes are supported: 124 * <code>Ellipse2D</code>, <code>Line2D</code> and <code>Rectangle2D</code> 125 * (implicit). 126 * 127 * @param s1 the first shape (<code>null</code> permitted). 128 * @param s2 the second shape (<code>null</code> permitted). 129 * 130 * @return A boolean. 131 */ 132 public static boolean equal(final Shape s1, final Shape s2) { 133 if (s1 instanceof Line2D && s2 instanceof Line2D) { 134 return equal((Line2D) s1, (Line2D) s2); 135 } 136 else if (s1 instanceof Ellipse2D && s2 instanceof Ellipse2D) { 137 return equal((Ellipse2D) s1, (Ellipse2D) s2); 138 } 139 else if (s1 instanceof Arc2D && s2 instanceof Arc2D) { 140 return equal((Arc2D) s1, (Arc2D) s2); 141 } 142 else if (s1 instanceof Polygon && s2 instanceof Polygon) { 143 return equal((Polygon) s1, (Polygon) s2); 144 } 145 else if (s1 instanceof GeneralPath && s2 instanceof GeneralPath) { 146 return equal((GeneralPath) s1, (GeneralPath) s2); 147 } 148 else { 149 // this will handle Rectangle2D... 150 return ObjectUtilities.equal(s1, s2); 151 } 152 } 153 154 /** 155 * Compares two lines are returns <code>true</code> if they are equal or 156 * both <code>null</code>. 157 * 158 * @param l1 the first line (<code>null</code> permitted). 159 * @param l2 the second line (<code>null</code> permitted). 160 * 161 * @return A boolean. 162 */ 163 public static boolean equal(final Line2D l1, final Line2D l2) { 164 if (l1 == null) { 165 return (l2 == null); 166 } 167 if (l2 == null) { 168 return false; 169 } 170 if (!l1.getP1().equals(l2.getP1())) { 171 return false; 172 } 173 if (!l1.getP2().equals(l2.getP2())) { 174 return false; 175 } 176 return true; 177 } 178 179 /** 180 * Compares two ellipses and returns <code>true</code> if they are equal or 181 * both <code>null</code>. 182 * 183 * @param e1 the first ellipse (<code>null</code> permitted). 184 * @param e2 the second ellipse (<code>null</code> permitted). 185 * 186 * @return A boolean. 187 */ 188 public static boolean equal(final Ellipse2D e1, final Ellipse2D e2) { 189 if (e1 == null) { 190 return (e2 == null); 191 } 192 if (e2 == null) { 193 return false; 194 } 195 if (!e1.getFrame().equals(e2.getFrame())) { 196 return false; 197 } 198 return true; 199 } 200 201 /** 202 * Compares two arcs and returns <code>true</code> if they are equal or 203 * both <code>null</code>. 204 * 205 * @param a1 the first arc (<code>null</code> permitted). 206 * @param a2 the second arc (<code>null</code> permitted). 207 * 208 * @return A boolean. 209 */ 210 public static boolean equal(final Arc2D a1, final Arc2D a2) { 211 if (a1 == null) { 212 return (a2 == null); 213 } 214 if (a2 == null) { 215 return false; 216 } 217 if (!a1.getFrame().equals(a2.getFrame())) { 218 return false; 219 } 220 if (a1.getAngleStart() != a2.getAngleStart()) { 221 return false; 222 } 223 if (a1.getAngleExtent() != a2.getAngleExtent()) { 224 return false; 225 } 226 if (a1.getArcType() != a2.getArcType()) { 227 return false; 228 } 229 return true; 230 } 231 232 /** 233 * Tests two polygons for equality. If both are <code>null</code> this 234 * method returns <code>true</code>. 235 * 236 * @param p1 polygon 1 (<code>null</code> permitted). 237 * @param p2 polygon 2 (<code>null</code> permitted). 238 * 239 * @return A boolean. 240 */ 241 public static boolean equal(final Polygon p1, final Polygon p2) { 242 if (p1 == null) { 243 return (p2 == null); 244 } 245 if (p2 == null) { 246 return false; 247 } 248 if (p1.npoints != p2.npoints) { 249 return false; 250 } 251 if (!Arrays.equals(p1.xpoints, p2.xpoints)) { 252 return false; 253 } 254 if (!Arrays.equals(p1.ypoints, p2.ypoints)) { 255 return false; 256 } 257 return true; 258 } 259 260 /** 261 * Tests two polygons for equality. If both are <code>null</code> this 262 * method returns <code>true</code>. 263 * 264 * @param p1 path 1 (<code>null</code> permitted). 265 * @param p2 path 2 (<code>null</code> permitted). 266 * 267 * @return A boolean. 268 */ 269 public static boolean equal(final GeneralPath p1, final GeneralPath p2) { 270 if (p1 == null) { 271 return (p2 == null); 272 } 273 if (p2 == null) { 274 return false; 275 } 276 if (p1.getWindingRule() != p2.getWindingRule()) { 277 return false; 278 } 279 PathIterator iterator1 = p1.getPathIterator(null); 280 PathIterator iterator2 = p1.getPathIterator(null); 281 double[] d1 = new double[6]; 282 double[] d2 = new double[6]; 283 boolean done = iterator1.isDone() && iterator2.isDone(); 284 while (!done) { 285 if (iterator1.isDone() != iterator2.isDone()) { 286 return false; 287 } 288 int seg1 = iterator1.currentSegment(d1); 289 int seg2 = iterator2.currentSegment(d2); 290 if (seg1 != seg2) { 291 return false; 292 } 293 if (!Arrays.equals(d1, d2)) { 294 return false; 295 } 296 iterator1.next(); 297 iterator2.next(); 298 done = iterator1.isDone() && iterator2.isDone(); 299 } 300 return true; 301 } 302 303 /** 304 * Creates and returns a translated shape. 305 * 306 * @param shape the shape (<code>null</code> not permitted). 307 * @param transX the x translation (in Java2D space). 308 * @param transY the y translation (in Java2D space). 309 * 310 * @return The translated shape. 311 */ 312 public static Shape createTranslatedShape(final Shape shape, 313 final double transX, 314 final double transY) { 315 if (shape == null) { 316 throw new IllegalArgumentException("Null 'shape' argument."); 317 } 318 final AffineTransform transform = AffineTransform.getTranslateInstance( 319 transX, transY 320 ); 321 return transform.createTransformedShape(shape); 322 } 323 324 /** 325 * Translates a shape to a new location such that the anchor point 326 * (relative to the rectangular bounds of the shape) aligns with the 327 * specified (x, y) coordinate in Java2D space. 328 * 329 * @param shape the shape (<code>null</code> not permitted). 330 * @param anchor the anchor (<code>null</code> not permitted). 331 * @param locationX the x-coordinate (in Java2D space). 332 * @param locationY the y-coordinate (in Java2D space). 333 * 334 * @return A new and translated shape. 335 */ 336 public static Shape createTranslatedShape(final Shape shape, 337 final RectangleAnchor anchor, 338 final double locationX, 339 final double locationY) { 340 if (shape == null) { 341 throw new IllegalArgumentException("Null 'shape' argument."); 342 } 343 if (anchor == null) { 344 throw new IllegalArgumentException("Null 'anchor' argument."); 345 } 346 Point2D anchorPoint = RectangleAnchor.coordinates( 347 shape.getBounds2D(), anchor 348 ); 349 final AffineTransform transform = AffineTransform.getTranslateInstance( 350 locationX - anchorPoint.getX(), locationY - anchorPoint.getY() 351 ); 352 return transform.createTransformedShape(shape); 353 } 354 355 /** 356 * Rotates a shape about the specified coordinates. 357 * 358 * @param base the shape (<code>null</code> permitted, returns 359 * <code>null</code>). 360 * @param angle the angle (in radians). 361 * @param x the x coordinate for the rotation point (in Java2D space). 362 * @param y the y coordinate for the rotation point (in Java2D space). 363 * 364 * @return the rotated shape. 365 */ 366 public static Shape rotateShape(final Shape base, final double angle, 367 final float x, final float y) { 368 if (base == null) { 369 return null; 370 } 371 final AffineTransform rotate = AffineTransform.getRotateInstance( 372 angle, x, y 373 ); 374 final Shape result = rotate.createTransformedShape(base); 375 return result; 376 } 377 378 /** 379 * Draws a shape with the specified rotation about <code>(x, y)</code>. 380 * 381 * @param g2 the graphics device (<code>null</code> not permitted). 382 * @param shape the shape (<code>null</code> not permitted). 383 * @param angle the angle (in radians). 384 * @param x the x coordinate for the rotation point. 385 * @param y the y coordinate for the rotation point. 386 */ 387 public static void drawRotatedShape(final Graphics2D g2, final Shape shape, 388 final double angle, 389 final float x, final float y) { 390 391 final AffineTransform saved = g2.getTransform(); 392 final AffineTransform rotate = AffineTransform.getRotateInstance( 393 angle, x, y 394 ); 395 g2.transform(rotate); 396 g2.draw(shape); 397 g2.setTransform(saved); 398 399 } 400 401 /** A useful constant used internally. */ 402 private static final float SQRT2 = (float) Math.pow(2.0, 0.5); 403 404 /** 405 * Creates a diagonal cross shape. 406 * 407 * @param l the length of each 'arm'. 408 * @param t the thickness. 409 * 410 * @return A diagonal cross shape. 411 */ 412 public static Shape createDiagonalCross(final float l, final float t) { 413 final GeneralPath p0 = new GeneralPath(); 414 p0.moveTo(-l - t, -l + t); 415 p0.lineTo(-l + t, -l - t); 416 p0.lineTo(0.0f, -t * SQRT2); 417 p0.lineTo(l - t, -l - t); 418 p0.lineTo(l + t, -l + t); 419 p0.lineTo(t * SQRT2, 0.0f); 420 p0.lineTo(l + t, l - t); 421 p0.lineTo(l - t, l + t); 422 p0.lineTo(0.0f, t * SQRT2); 423 p0.lineTo(-l + t, l + t); 424 p0.lineTo(-l - t, l - t); 425 p0.lineTo(-t * SQRT2, 0.0f); 426 p0.closePath(); 427 return p0; 428 } 429 430 /** 431 * Creates a diagonal cross shape. 432 * 433 * @param l the length of each 'arm'. 434 * @param t the thickness. 435 * 436 * @return A diagonal cross shape. 437 */ 438 public static Shape createRegularCross(final float l, final float t) { 439 final GeneralPath p0 = new GeneralPath(); 440 p0.moveTo(-l, t); 441 p0.lineTo(-t, t); 442 p0.lineTo(-t, l); 443 p0.lineTo(t, l); 444 p0.lineTo(t, t); 445 p0.lineTo(l, t); 446 p0.lineTo(l, -t); 447 p0.lineTo(t, -t); 448 p0.lineTo(t, -l); 449 p0.lineTo(-t, -l); 450 p0.lineTo(-t, -t); 451 p0.lineTo(-l, -t); 452 p0.closePath(); 453 return p0; 454 } 455 456 /** 457 * Creates a diamond shape. 458 * 459 * @param s the size factor (equal to half the height of the diamond). 460 * 461 * @return A diamond shape. 462 */ 463 public static Shape createDiamond(final float s) { 464 final GeneralPath p0 = new GeneralPath(); 465 p0.moveTo(0.0f, -s); 466 p0.lineTo(s, 0.0f); 467 p0.lineTo(0.0f, s); 468 p0.lineTo(-s, 0.0f); 469 p0.closePath(); 470 return p0; 471 } 472 473 /** 474 * Creates a triangle shape that points upwards. 475 * 476 * @param s the size factor (equal to half the height of the triangle). 477 * 478 * @return A triangle shape. 479 */ 480 public static Shape createUpTriangle(final float s) { 481 final GeneralPath p0 = new GeneralPath(); 482 p0.moveTo(0.0f, -s); 483 p0.lineTo(s, s); 484 p0.lineTo(-s, s); 485 p0.closePath(); 486 return p0; 487 } 488 489 /** 490 * Creates a triangle shape that points downwards. 491 * 492 * @param s the size factor (equal to half the height of the triangle). 493 * 494 * @return A triangle shape. 495 */ 496 public static Shape createDownTriangle(final float s) { 497 final GeneralPath p0 = new GeneralPath(); 498 p0.moveTo(0.0f, s); 499 p0.lineTo(s, -s); 500 p0.lineTo(-s, -s); 501 p0.closePath(); 502 return p0; 503 } 504 505 /** 506 * Creates a region surrounding a line segment by 'widening' the line 507 * segment. A typical use for this method is the creation of a 508 * 'clickable' region for a line that is displayed on-screen. 509 * 510 * @param line the line (<code>null</code> not permitted). 511 * @param width the width of the region. 512 * 513 * @return A region that surrounds the line. 514 */ 515 public static Shape createLineRegion(final Line2D line, final float width) { 516 final GeneralPath result = new GeneralPath(); 517 final float x1 = (float) line.getX1(); 518 final float x2 = (float) line.getX2(); 519 final float y1 = (float) line.getY1(); 520 final float y2 = (float) line.getY2(); 521 if ((x2 - x1) != 0.0) { 522 final double theta = Math.atan((y2 - y1) / (x2 - x1)); 523 final float dx = (float) Math.sin(theta) * width; 524 final float dy = (float) Math.cos(theta) * width; 525 result.moveTo(x1 - dx, y1 + dy); 526 result.lineTo(x1 + dx, y1 - dy); 527 result.lineTo(x2 + dx, y2 - dy); 528 result.lineTo(x2 - dx, y2 + dy); 529 result.closePath(); 530 } 531 else { 532 // special case, vertical line 533 result.moveTo(x1 - width / 2.0f, y1); 534 result.lineTo(x1 + width / 2.0f, y1); 535 result.lineTo(x2 + width / 2.0f, y2); 536 result.lineTo(x2 - width / 2.0f, y2); 537 result.closePath(); 538 } 539 return result; 540 } 541 542 /** 543 * Returns a point based on (x, y) but constrained to be within the bounds 544 * of a given rectangle. 545 * 546 * @param x the x-coordinate. 547 * @param y the y-coordinate. 548 * @param area the constraining rectangle (<code>null</code> not 549 * permitted). 550 * 551 * @return A point within the rectangle. 552 * 553 * @throws NullPointerException if <code>area</code> is <code>null</code>. 554 */ 555 public static Point2D getPointInRectangle(double x, double y, 556 final Rectangle2D area) { 557 558 x = Math.max(area.getMinX(), Math.min(x, area.getMaxX())); 559 y = Math.max(area.getMinY(), Math.min(y, area.getMaxY())); 560 return new Point2D.Double(x, y); 561 562 } 563 564 /** 565 * Checks, whether the given rectangle1 fully contains rectangle 2 566 * (even if rectangle 2 has a height or width of zero!). 567 * 568 * @param rect1 the first rectangle. 569 * @param rect2 the second rectangle. 570 * 571 * @return A boolean. 572 */ 573 public static boolean contains(final Rectangle2D rect1, 574 final Rectangle2D rect2) { 575 576 final double x0 = rect1.getX(); 577 final double y0 = rect1.getY(); 578 final double x = rect2.getX(); 579 final double y = rect2.getY(); 580 final double w = rect2.getWidth(); 581 final double h = rect2.getHeight(); 582 583 return ((x >= x0) && (y >= y0) 584 && ((x + w) <= (x0 + rect1.getWidth())) 585 && ((y + h) <= (y0 + rect1.getHeight()))); 586 587 } 588 589 590 /** 591 * Checks, whether the given rectangle1 fully contains rectangle 2 592 * (even if rectangle 2 has a height or width of zero!). 593 * 594 * @param rect1 the first rectangle. 595 * @param rect2 the second rectangle. 596 * 597 * @return A boolean. 598 */ 599 public static boolean intersects (final Rectangle2D rect1, 600 final Rectangle2D rect2) { 601 602 final double x0 = rect1.getX(); 603 final double y0 = rect1.getY(); 604 605 final double x = rect2.getX(); 606 final double width = rect2.getWidth(); 607 final double y = rect2.getY(); 608 final double height = rect2.getHeight(); 609 return (x + width >= x0 && y + height >= y0 && x <= x0 + rect1.getWidth() 610 && y <= y0 + rect1.getHeight()); 611 } 612 }