001/* ======================================================================== 002 * JCommon : a free general purpose class library for the Java(tm) platform 003 * ======================================================================== 004 * 005 * (C) Copyright 2000-2008, 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-2008, 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.18 2008/06/02 06:58:28 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 * 01-Jun-2008 : Fixed bug in equal(GeneralPath, GeneralPath) method (DG); 059 * 060 */ 061 062package org.jfree.util; 063 064import java.awt.Graphics2D; 065import java.awt.Polygon; 066import java.awt.Shape; 067import java.awt.geom.AffineTransform; 068import java.awt.geom.Arc2D; 069import java.awt.geom.Ellipse2D; 070import java.awt.geom.GeneralPath; 071import java.awt.geom.Line2D; 072import java.awt.geom.PathIterator; 073import java.awt.geom.Point2D; 074import java.awt.geom.Rectangle2D; 075import java.util.Arrays; 076 077import org.jfree.ui.RectangleAnchor; 078 079/** 080 * Utility methods for {@link Shape} objects. 081 * 082 * @author David Gilbert 083 */ 084public class ShapeUtilities { 085 086 /** 087 * Prevents instantiation. 088 */ 089 private ShapeUtilities() { 090 } 091 092 /** 093 * Returns a clone of the specified shape, or <code>null</code>. At the 094 * current time, this method supports cloning for instances of 095 * <code>Line2D</code>, <code>RectangularShape</code>, <code>Area</code> 096 * and <code>GeneralPath</code>. 097 * <p> 098 * <code>RectangularShape</code> includes <code>Arc2D</code>, 099 * <code>Ellipse2D</code>, <code>Rectangle2D</code>, 100 * <code>RoundRectangle2D</code>. 101 * 102 * @param shape the shape to clone (<code>null</code> permitted, 103 * returns <code>null</code>). 104 * 105 * @return A clone or <code>null</code>. 106 */ 107 public static Shape clone(final Shape shape) { 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 = p2.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 return transform.createTransformedShape(shape); 321 } 322 323 /** 324 * Translates a shape to a new location such that the anchor point 325 * (relative to the rectangular bounds of the shape) aligns with the 326 * specified (x, y) coordinate in Java2D space. 327 * 328 * @param shape the shape (<code>null</code> not permitted). 329 * @param anchor the anchor (<code>null</code> not permitted). 330 * @param locationX the x-coordinate (in Java2D space). 331 * @param locationY the y-coordinate (in Java2D space). 332 * 333 * @return A new and translated shape. 334 */ 335 public static Shape createTranslatedShape(final Shape shape, 336 final RectangleAnchor anchor, 337 final double locationX, 338 final double locationY) { 339 if (shape == null) { 340 throw new IllegalArgumentException("Null 'shape' argument."); 341 } 342 if (anchor == null) { 343 throw new IllegalArgumentException("Null 'anchor' argument."); 344 } 345 Point2D anchorPoint = RectangleAnchor.coordinates( 346 shape.getBounds2D(), anchor); 347 final AffineTransform transform = AffineTransform.getTranslateInstance( 348 locationX - anchorPoint.getX(), locationY - anchorPoint.getY()); 349 return transform.createTransformedShape(shape); 350 } 351 352 /** 353 * Rotates a shape about the specified coordinates. 354 * 355 * @param base the shape (<code>null</code> permitted, returns 356 * <code>null</code>). 357 * @param angle the angle (in radians). 358 * @param x the x coordinate for the rotation point (in Java2D space). 359 * @param y the y coordinate for the rotation point (in Java2D space). 360 * 361 * @return the rotated shape. 362 */ 363 public static Shape rotateShape(final Shape base, final double angle, 364 final float x, final float y) { 365 if (base == null) { 366 return null; 367 } 368 final AffineTransform rotate = AffineTransform.getRotateInstance( 369 angle, x, y); 370 final Shape result = rotate.createTransformedShape(base); 371 return result; 372 } 373 374 /** 375 * Draws a shape with the specified rotation about <code>(x, y)</code>. 376 * 377 * @param g2 the graphics device (<code>null</code> not permitted). 378 * @param shape the shape (<code>null</code> not permitted). 379 * @param angle the angle (in radians). 380 * @param x the x coordinate for the rotation point. 381 * @param y the y coordinate for the rotation point. 382 */ 383 public static void drawRotatedShape(final Graphics2D g2, final Shape shape, 384 final double angle, 385 final float x, final float y) { 386 387 final AffineTransform saved = g2.getTransform(); 388 final AffineTransform rotate = AffineTransform.getRotateInstance( 389 angle, x, y); 390 g2.transform(rotate); 391 g2.draw(shape); 392 g2.setTransform(saved); 393 394 } 395 396 /** A useful constant used internally. */ 397 private static final float SQRT2 = (float) Math.pow(2.0, 0.5); 398 399 /** 400 * Creates a diagonal cross shape. 401 * 402 * @param l the length of each 'arm'. 403 * @param t the thickness. 404 * 405 * @return A diagonal cross shape. 406 */ 407 public static Shape createDiagonalCross(final float l, final float t) { 408 final GeneralPath p0 = new GeneralPath(); 409 p0.moveTo(-l - t, -l + t); 410 p0.lineTo(-l + t, -l - t); 411 p0.lineTo(0.0f, -t * SQRT2); 412 p0.lineTo(l - t, -l - t); 413 p0.lineTo(l + t, -l + t); 414 p0.lineTo(t * SQRT2, 0.0f); 415 p0.lineTo(l + t, l - t); 416 p0.lineTo(l - t, l + t); 417 p0.lineTo(0.0f, t * SQRT2); 418 p0.lineTo(-l + t, l + t); 419 p0.lineTo(-l - t, l - t); 420 p0.lineTo(-t * SQRT2, 0.0f); 421 p0.closePath(); 422 return p0; 423 } 424 425 /** 426 * Creates a diagonal cross shape. 427 * 428 * @param l the length of each 'arm'. 429 * @param t the thickness. 430 * 431 * @return A diagonal cross shape. 432 */ 433 public static Shape createRegularCross(final float l, final float t) { 434 final GeneralPath p0 = new GeneralPath(); 435 p0.moveTo(-l, t); 436 p0.lineTo(-t, t); 437 p0.lineTo(-t, l); 438 p0.lineTo(t, l); 439 p0.lineTo(t, t); 440 p0.lineTo(l, t); 441 p0.lineTo(l, -t); 442 p0.lineTo(t, -t); 443 p0.lineTo(t, -l); 444 p0.lineTo(-t, -l); 445 p0.lineTo(-t, -t); 446 p0.lineTo(-l, -t); 447 p0.closePath(); 448 return p0; 449 } 450 451 /** 452 * Creates a diamond shape. 453 * 454 * @param s the size factor (equal to half the height of the diamond). 455 * 456 * @return A diamond shape. 457 */ 458 public static Shape createDiamond(final float s) { 459 final GeneralPath p0 = new GeneralPath(); 460 p0.moveTo(0.0f, -s); 461 p0.lineTo(s, 0.0f); 462 p0.lineTo(0.0f, s); 463 p0.lineTo(-s, 0.0f); 464 p0.closePath(); 465 return p0; 466 } 467 468 /** 469 * Creates a triangle shape that points upwards. 470 * 471 * @param s the size factor (equal to half the height of the triangle). 472 * 473 * @return A triangle shape. 474 */ 475 public static Shape createUpTriangle(final float s) { 476 final GeneralPath p0 = new GeneralPath(); 477 p0.moveTo(0.0f, -s); 478 p0.lineTo(s, s); 479 p0.lineTo(-s, s); 480 p0.closePath(); 481 return p0; 482 } 483 484 /** 485 * Creates a triangle shape that points downwards. 486 * 487 * @param s the size factor (equal to half the height of the triangle). 488 * 489 * @return A triangle shape. 490 */ 491 public static Shape createDownTriangle(final float s) { 492 final GeneralPath p0 = new GeneralPath(); 493 p0.moveTo(0.0f, s); 494 p0.lineTo(s, -s); 495 p0.lineTo(-s, -s); 496 p0.closePath(); 497 return p0; 498 } 499 500 /** 501 * Creates a region surrounding a line segment by 'widening' the line 502 * segment. A typical use for this method is the creation of a 503 * 'clickable' region for a line that is displayed on-screen. 504 * 505 * @param line the line (<code>null</code> not permitted). 506 * @param width the width of the region. 507 * 508 * @return A region that surrounds the line. 509 */ 510 public static Shape createLineRegion(final Line2D line, final float width) { 511 final GeneralPath result = new GeneralPath(); 512 final float x1 = (float) line.getX1(); 513 final float x2 = (float) line.getX2(); 514 final float y1 = (float) line.getY1(); 515 final float y2 = (float) line.getY2(); 516 if ((x2 - x1) != 0.0) { 517 final double theta = Math.atan((y2 - y1) / (x2 - x1)); 518 final float dx = (float) Math.sin(theta) * width; 519 final float dy = (float) Math.cos(theta) * width; 520 result.moveTo(x1 - dx, y1 + dy); 521 result.lineTo(x1 + dx, y1 - dy); 522 result.lineTo(x2 + dx, y2 - dy); 523 result.lineTo(x2 - dx, y2 + dy); 524 result.closePath(); 525 } 526 else { 527 // special case, vertical line 528 result.moveTo(x1 - width / 2.0f, y1); 529 result.lineTo(x1 + width / 2.0f, y1); 530 result.lineTo(x2 + width / 2.0f, y2); 531 result.lineTo(x2 - width / 2.0f, y2); 532 result.closePath(); 533 } 534 return result; 535 } 536 537 /** 538 * Returns a point based on (x, y) but constrained to be within the bounds 539 * of a given rectangle. 540 * 541 * @param x the x-coordinate. 542 * @param y the y-coordinate. 543 * @param area the constraining rectangle (<code>null</code> not 544 * permitted). 545 * 546 * @return A point within the rectangle. 547 * 548 * @throws NullPointerException if <code>area</code> is <code>null</code>. 549 */ 550 public static Point2D getPointInRectangle(double x, double y, 551 final Rectangle2D area) { 552 553 x = Math.max(area.getMinX(), Math.min(x, area.getMaxX())); 554 y = Math.max(area.getMinY(), Math.min(y, area.getMaxY())); 555 return new Point2D.Double(x, y); 556 557 } 558 559 /** 560 * Checks, whether the given rectangle1 fully contains rectangle 2 561 * (even if rectangle 2 has a height or width of zero!). 562 * 563 * @param rect1 the first rectangle. 564 * @param rect2 the second rectangle. 565 * 566 * @return A boolean. 567 */ 568 public static boolean contains(final Rectangle2D rect1, 569 final Rectangle2D rect2) { 570 571 final double x0 = rect1.getX(); 572 final double y0 = rect1.getY(); 573 final double x = rect2.getX(); 574 final double y = rect2.getY(); 575 final double w = rect2.getWidth(); 576 final double h = rect2.getHeight(); 577 578 return ((x >= x0) && (y >= y0) 579 && ((x + w) <= (x0 + rect1.getWidth())) 580 && ((y + h) <= (y0 + rect1.getHeight()))); 581 582 } 583 584 585 /** 586 * Checks, whether the given rectangle1 fully contains rectangle 2 587 * (even if rectangle 2 has a height or width of zero!). 588 * 589 * @param rect1 the first rectangle. 590 * @param rect2 the second rectangle. 591 * 592 * @return A boolean. 593 */ 594 public static boolean intersects (final Rectangle2D rect1, 595 final Rectangle2D rect2) { 596 597 final double x0 = rect1.getX(); 598 final double y0 = rect1.getY(); 599 600 final double x = rect2.getX(); 601 final double width = rect2.getWidth(); 602 final double y = rect2.getY(); 603 final double height = rect2.getHeight(); 604 return (x + width >= x0 && y + height >= y0 && x <= x0 + rect1.getWidth() 605 && y <= y0 + rect1.getHeight()); 606 } 607 608}