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