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