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 * AbstractBlock.java 029 * ------------------ 030 * (C) Copyright 2004-present, by David Gilbert. 031 * 032 * Original Author: David Gilbert; 033 * Contributor(s): Tracy Hiltbrand (equals/hashCode comply with EqualsVerifier); 034 * 035 */ 036 037package org.jfree.chart.block; 038 039import java.awt.Graphics2D; 040import java.awt.geom.Rectangle2D; 041import java.io.IOException; 042import java.io.ObjectInputStream; 043import java.io.ObjectOutputStream; 044import java.io.Serializable; 045import java.util.Objects; 046import org.jfree.chart.ui.RectangleInsets; 047import org.jfree.chart.ui.Size2D; 048import org.jfree.chart.util.Args; 049import org.jfree.chart.util.PublicCloneable; 050import org.jfree.chart.util.SerialUtils; 051import org.jfree.chart.util.ShapeUtils; 052 053import org.jfree.data.Range; 054 055/** 056 * A convenience class for creating new classes that implement 057 * the {@link Block} interface. 058 */ 059public class AbstractBlock implements Cloneable, Serializable { 060 061 /** For serialization. */ 062 private static final long serialVersionUID = 7689852412141274563L; 063 064 /** The id for the block. */ 065 private String id; 066 067 /** The margin around the outside of the block. */ 068 private RectangleInsets margin; 069 070 /** The frame (or border) for the block. */ 071 private BlockFrame frame; 072 073 /** The padding between the block content and the border. */ 074 private RectangleInsets padding; 075 076 /** 077 * The natural width of the block (may be overridden if there are 078 * constraints in sizing). 079 */ 080 private double width; 081 082 /** 083 * The natural height of the block (may be overridden if there are 084 * constraints in sizing). 085 */ 086 private double height; 087 088 /** 089 * The current bounds for the block (position of the block in Java2D space). 090 */ 091 private transient Rectangle2D bounds; 092 093 /** 094 * Creates a new block. 095 */ 096 protected AbstractBlock() { 097 this.id = null; 098 this.width = 0.0; 099 this.height = 0.0; 100 this.bounds = new Rectangle2D.Float(); 101 this.margin = RectangleInsets.ZERO_INSETS; 102 this.frame = BlockBorder.NONE; 103 this.padding = RectangleInsets.ZERO_INSETS; 104 } 105 106 /** 107 * Returns the id. 108 * 109 * @return The id (possibly {@code null}). 110 * 111 * @see #setID(String) 112 */ 113 public String getID() { 114 return this.id; 115 } 116 117 /** 118 * Sets the id for the block. 119 * 120 * @param id the id ({@code null} permitted). 121 * 122 * @see #getID() 123 */ 124 public void setID(String id) { 125 this.id = id; 126 } 127 128 /** 129 * Returns the natural width of the block, if this is known in advance. 130 * The actual width of the block may be overridden if layout constraints 131 * make this necessary. 132 * 133 * @return The width. 134 * 135 * @see #setWidth(double) 136 */ 137 public double getWidth() { 138 return this.width; 139 } 140 141 /** 142 * Sets the natural width of the block, if this is known in advance. 143 * 144 * @param width the width (in Java2D units) 145 * 146 * @see #getWidth() 147 */ 148 public void setWidth(double width) { 149 this.width = width; 150 } 151 152 /** 153 * Returns the natural height of the block, if this is known in advance. 154 * The actual height of the block may be overridden if layout constraints 155 * make this necessary. 156 * 157 * @return The height. 158 * 159 * @see #setHeight(double) 160 */ 161 public double getHeight() { 162 return this.height; 163 } 164 165 /** 166 * Sets the natural width of the block, if this is known in advance. 167 * 168 * @param height the width (in Java2D units) 169 * 170 * @see #getHeight() 171 */ 172 public void setHeight(double height) { 173 this.height = height; 174 } 175 176 /** 177 * Returns the margin. 178 * 179 * @return The margin (never {@code null}). 180 * 181 * @see #getMargin() 182 */ 183 public RectangleInsets getMargin() { 184 return this.margin; 185 } 186 187 /** 188 * Sets the margin (use {@link RectangleInsets#ZERO_INSETS} for no 189 * padding). 190 * 191 * @param margin the margin ({@code null} not permitted). 192 * 193 * @see #getMargin() 194 */ 195 public void setMargin(RectangleInsets margin) { 196 Args.nullNotPermitted(margin, "margin"); 197 this.margin = margin; 198 } 199 200 /** 201 * Sets the margin. 202 * 203 * @param top the top margin. 204 * @param left the left margin. 205 * @param bottom the bottom margin. 206 * @param right the right margin. 207 * 208 * @see #getMargin() 209 */ 210 public void setMargin(double top, double left, double bottom, 211 double right) { 212 setMargin(new RectangleInsets(top, left, bottom, right)); 213 } 214 215 /** 216 * Sets a black border with the specified line widths. 217 * 218 * @param top the top border line width. 219 * @param left the left border line width. 220 * @param bottom the bottom border line width. 221 * @param right the right border line width. 222 */ 223 public void setBorder(double top, double left, double bottom, 224 double right) { 225 setFrame(new BlockBorder(top, left, bottom, right)); 226 } 227 228 /** 229 * Returns the current frame (border). 230 * 231 * @return The frame. 232 * 233 * @see #setFrame(BlockFrame) 234 */ 235 public BlockFrame getFrame() { 236 return this.frame; 237 } 238 239 /** 240 * Sets the frame (or border). 241 * 242 * @param frame the frame ({@code null} not permitted). 243 * 244 * @see #getFrame() 245 */ 246 public void setFrame(BlockFrame frame) { 247 Args.nullNotPermitted(frame, "frame"); 248 this.frame = frame; 249 } 250 251 /** 252 * Returns the padding. 253 * 254 * @return The padding (never {@code null}). 255 * 256 * @see #setPadding(RectangleInsets) 257 */ 258 public RectangleInsets getPadding() { 259 return this.padding; 260 } 261 262 /** 263 * Sets the padding (use {@link RectangleInsets#ZERO_INSETS} for no 264 * padding). 265 * 266 * @param padding the padding ({@code null} not permitted). 267 * 268 * @see #getPadding() 269 */ 270 public void setPadding(RectangleInsets padding) { 271 Args.nullNotPermitted(padding, "padding"); 272 this.padding = padding; 273 } 274 275 /** 276 * Sets the padding. 277 * 278 * @param top the top padding. 279 * @param left the left padding. 280 * @param bottom the bottom padding. 281 * @param right the right padding. 282 */ 283 public void setPadding(double top, double left, double bottom, 284 double right) { 285 setPadding(new RectangleInsets(top, left, bottom, right)); 286 } 287 288 /** 289 * Returns the x-offset for the content within the block. 290 * 291 * @return The x-offset. 292 * 293 * @see #getContentYOffset() 294 */ 295 public double getContentXOffset() { 296 return this.margin.getLeft() + this.frame.getInsets().getLeft() 297 + this.padding.getLeft(); 298 } 299 300 /** 301 * Returns the y-offset for the content within the block. 302 * 303 * @return The y-offset. 304 * 305 * @see #getContentXOffset() 306 */ 307 public double getContentYOffset() { 308 return this.margin.getTop() + this.frame.getInsets().getTop() 309 + this.padding.getTop(); 310 } 311 312 /** 313 * Arranges the contents of the block, with no constraints, and returns 314 * the block size. 315 * 316 * @param g2 the graphics device. 317 * 318 * @return The block size (in Java2D units, never {@code null}). 319 */ 320 public Size2D arrange(Graphics2D g2) { 321 return arrange(g2, RectangleConstraint.NONE); 322 } 323 324 /** 325 * Arranges the contents of the block, within the given constraints, and 326 * returns the block size. 327 * 328 * @param g2 the graphics device. 329 * @param constraint the constraint ({@code null} not permitted). 330 * 331 * @return The block size (in Java2D units, never {@code null}). 332 */ 333 public Size2D arrange(Graphics2D g2, RectangleConstraint constraint) { 334 Size2D base = new Size2D(getWidth(), getHeight()); 335 return constraint.calculateConstrainedSize(base); 336 } 337 338 /** 339 * Returns the current bounds of the block. 340 * 341 * @return The bounds. 342 * 343 * @see #setBounds(Rectangle2D) 344 */ 345 public Rectangle2D getBounds() { 346 return this.bounds; 347 } 348 349 /** 350 * Sets the bounds of the block. 351 * 352 * @param bounds the bounds ({@code null} not permitted). 353 * 354 * @see #getBounds() 355 */ 356 public void setBounds(Rectangle2D bounds) { 357 Args.nullNotPermitted(bounds, "bounds"); 358 this.bounds = bounds; 359 } 360 361 /** 362 * Calculate the width available for content after subtracting 363 * the margin, border and padding space from the specified fixed 364 * width. 365 * 366 * @param fixedWidth the fixed width. 367 * 368 * @return The available space. 369 * 370 * @see #trimToContentHeight(double) 371 */ 372 protected double trimToContentWidth(double fixedWidth) { 373 double result = this.margin.trimWidth(fixedWidth); 374 result = this.frame.getInsets().trimWidth(result); 375 result = this.padding.trimWidth(result); 376 return Math.max(result, 0.0); 377 } 378 379 /** 380 * Calculate the height available for content after subtracting 381 * the margin, border and padding space from the specified fixed 382 * height. 383 * 384 * @param fixedHeight the fixed height. 385 * 386 * @return The available space. 387 * 388 * @see #trimToContentWidth(double) 389 */ 390 protected double trimToContentHeight(double fixedHeight) { 391 double result = this.margin.trimHeight(fixedHeight); 392 result = this.frame.getInsets().trimHeight(result); 393 result = this.padding.trimHeight(result); 394 return Math.max(result, 0.0); 395 } 396 397 /** 398 * Returns a constraint for the content of this block that will result in 399 * the bounds of the block matching the specified constraint. 400 * 401 * @param c the outer constraint ({@code null} not permitted). 402 * 403 * @return The content constraint. 404 */ 405 protected RectangleConstraint toContentConstraint(RectangleConstraint c) { 406 Args.nullNotPermitted(c, "c"); 407 if (c.equals(RectangleConstraint.NONE)) { 408 return c; 409 } 410 double w = c.getWidth(); 411 Range wr = c.getWidthRange(); 412 double h = c.getHeight(); 413 Range hr = c.getHeightRange(); 414 double ww = trimToContentWidth(w); 415 double hh = trimToContentHeight(h); 416 Range wwr = trimToContentWidth(wr); 417 Range hhr = trimToContentHeight(hr); 418 return new RectangleConstraint(ww, wwr, c.getWidthConstraintType(), 419 hh, hhr, c.getHeightConstraintType()); 420 } 421 422 private Range trimToContentWidth(Range r) { 423 if (r == null) { 424 return null; 425 } 426 double lowerBound = 0.0; 427 double upperBound = Double.POSITIVE_INFINITY; 428 if (r.getLowerBound() > 0.0) { 429 lowerBound = trimToContentWidth(r.getLowerBound()); 430 } 431 if (r.getUpperBound() < Double.POSITIVE_INFINITY) { 432 upperBound = trimToContentWidth(r.getUpperBound()); 433 } 434 return new Range(lowerBound, upperBound); 435 } 436 437 private Range trimToContentHeight(Range r) { 438 if (r == null) { 439 return null; 440 } 441 double lowerBound = 0.0; 442 double upperBound = Double.POSITIVE_INFINITY; 443 if (r.getLowerBound() > 0.0) { 444 lowerBound = trimToContentHeight(r.getLowerBound()); 445 } 446 if (r.getUpperBound() < Double.POSITIVE_INFINITY) { 447 upperBound = trimToContentHeight(r.getUpperBound()); 448 } 449 return new Range(lowerBound, upperBound); 450 } 451 452 /** 453 * Adds the margin, border and padding to the specified content width. 454 * 455 * @param contentWidth the content width. 456 * 457 * @return The adjusted width. 458 */ 459 protected double calculateTotalWidth(double contentWidth) { 460 double result = contentWidth; 461 result = this.padding.extendWidth(result); 462 result = this.frame.getInsets().extendWidth(result); 463 result = this.margin.extendWidth(result); 464 return result; 465 } 466 467 /** 468 * Adds the margin, border and padding to the specified content height. 469 * 470 * @param contentHeight the content height. 471 * 472 * @return The adjusted height. 473 */ 474 protected double calculateTotalHeight(double contentHeight) { 475 double result = contentHeight; 476 result = this.padding.extendHeight(result); 477 result = this.frame.getInsets().extendHeight(result); 478 result = this.margin.extendHeight(result); 479 return result; 480 } 481 482 /** 483 * Reduces the specified area by the amount of space consumed 484 * by the margin. 485 * 486 * @param area the area ({@code null} not permitted). 487 * 488 * @return The trimmed area. 489 */ 490 protected Rectangle2D trimMargin(Rectangle2D area) { 491 // defer argument checking... 492 this.margin.trim(area); 493 return area; 494 } 495 496 /** 497 * Reduces the specified area by the amount of space consumed 498 * by the border. 499 * 500 * @param area the area ({@code null} not permitted). 501 * 502 * @return The trimmed area. 503 */ 504 protected Rectangle2D trimBorder(Rectangle2D area) { 505 // defer argument checking... 506 this.frame.getInsets().trim(area); 507 return area; 508 } 509 510 /** 511 * Reduces the specified area by the amount of space consumed 512 * by the padding. 513 * 514 * @param area the area ({@code null} not permitted). 515 * 516 * @return The trimmed area. 517 */ 518 protected Rectangle2D trimPadding(Rectangle2D area) { 519 // defer argument checking... 520 this.padding.trim(area); 521 return area; 522 } 523 524 /** 525 * Draws the border around the perimeter of the specified area. 526 * 527 * @param g2 the graphics device. 528 * @param area the area. 529 */ 530 protected void drawBorder(Graphics2D g2, Rectangle2D area) { 531 this.frame.draw(g2, area); 532 } 533 534 /** 535 * Tests this block for equality with an arbitrary object. 536 * 537 * @param obj the object ({@code null} permitted). 538 * 539 * @return A boolean. 540 */ 541 @Override 542 public boolean equals(Object obj) { 543 if (obj == this) { 544 return true; 545 } 546 if (!(obj instanceof AbstractBlock)) { 547 return false; 548 } 549 AbstractBlock that = (AbstractBlock) obj; 550 if (!Objects.equals(this.id, that.id)) { 551 return false; 552 } 553 if (!Objects.equals(this.frame, that.frame)) { 554 return false; 555 } 556 if (!Objects.equals(this.bounds, that.bounds)) { 557 return false; 558 } 559 if (!Objects.equals(this.margin, that.margin)) { 560 return false; 561 } 562 if (!Objects.equals(this.padding, that.padding)) { 563 return false; 564 } 565 if (Double.doubleToLongBits(this.height) != 566 Double.doubleToLongBits(that.height)) { 567 return false; 568 } 569 if (Double.doubleToLongBits(this.width) != 570 Double.doubleToLongBits(that.width)) { 571 return false; 572 } 573 574 // fix the "equals not symmetric" problem 575 if (!that.canEqual(this)) { 576 return false; 577 } 578 return true; 579 } 580 581 /** 582 * Ensures symmetry between super/subclass implementations of equals. For 583 * more detail, see http://jqno.nl/equalsverifier/manual/inheritance. 584 * 585 * @param other Object 586 * 587 * @return true ONLY if the parameter is THIS class type 588 */ 589 public boolean canEqual(Object other) { 590 // fix the "equals not symmetric" problem 591 return (other instanceof AbstractBlock); 592 } 593 594 @Override 595 public int hashCode() { 596 int hash = 3; 597 hash = 89 * hash + Objects.hashCode(this.id); 598 hash = 89 * hash + Objects.hashCode(this.margin); 599 hash = 89 * hash + Objects.hashCode(this.frame); 600 hash = 89 * hash + Objects.hashCode(this.padding); 601 hash = 89 * hash + Objects.hashCode(this.bounds); 602 hash = 89 * hash + 603 (int) (Double.doubleToLongBits(this.width) ^ 604 (Double.doubleToLongBits(this.width) >>> 32)); 605 hash = 89 * hash + 606 (int) (Double.doubleToLongBits(this.height) ^ 607 (Double.doubleToLongBits(this.height) >>> 32)); 608 return hash; 609 } 610 611 /** 612 * Returns a clone of this block. 613 * 614 * @return A clone. 615 * 616 * @throws CloneNotSupportedException if there is a problem creating the 617 * clone. 618 */ 619 @Override 620 public Object clone() throws CloneNotSupportedException { 621 AbstractBlock clone = (AbstractBlock) super.clone(); 622 clone.bounds = (Rectangle2D) ShapeUtils.clone(this.bounds); 623 if (this.frame instanceof PublicCloneable) { 624 PublicCloneable pc = (PublicCloneable) this.frame; 625 clone.frame = (BlockFrame) pc.clone(); 626 } 627 return clone; 628 } 629 630 /** 631 * Provides serialization support. 632 * 633 * @param stream the output stream. 634 * 635 * @throws IOException if there is an I/O error. 636 */ 637 private void writeObject(ObjectOutputStream stream) throws IOException { 638 stream.defaultWriteObject(); 639 SerialUtils.writeShape(this.bounds, stream); 640 } 641 642 /** 643 * Provides serialization support. 644 * 645 * @param stream the input stream. 646 * 647 * @throws IOException if there is an I/O error. 648 * @throws ClassNotFoundException if there is a classpath problem. 649 */ 650 private void readObject(ObjectInputStream stream) 651 throws IOException, ClassNotFoundException { 652 stream.defaultReadObject(); 653 this.bounds = (Rectangle2D) SerialUtils.readShape(stream); 654 } 655 656}