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 * DialValueIndicator.java 029 * ----------------------- 030 * (C) Copyright 2006-2020, by Object Refinery Limited. 031 * 032 * Original Author: David Gilbert (for Object Refinery Limited); 033 * Contributor(s): -; 034 * 035 */ 036 037package org.jfree.chart.plot.dial; 038 039import java.awt.BasicStroke; 040import java.awt.Color; 041import java.awt.Font; 042import java.awt.FontMetrics; 043import java.awt.Graphics2D; 044import java.awt.Paint; 045import java.awt.Shape; 046import java.awt.Stroke; 047import java.awt.geom.Arc2D; 048import java.awt.geom.Point2D; 049import java.awt.geom.Rectangle2D; 050import java.io.IOException; 051import java.io.ObjectInputStream; 052import java.io.ObjectOutputStream; 053import java.io.Serializable; 054import java.text.DecimalFormat; 055import java.text.NumberFormat; 056 057import org.jfree.chart.HashUtils; 058import org.jfree.chart.text.TextUtils; 059import org.jfree.chart.ui.RectangleAnchor; 060import org.jfree.chart.ui.RectangleInsets; 061import org.jfree.chart.ui.Size2D; 062import org.jfree.chart.ui.TextAnchor; 063import org.jfree.chart.util.ObjectUtils; 064import org.jfree.chart.util.PaintUtils; 065import org.jfree.chart.util.Args; 066import org.jfree.chart.util.PublicCloneable; 067import org.jfree.chart.util.SerialUtils; 068 069/** 070 * A value indicator for a {@link DialPlot}. 071 * 072 * @since 1.0.7 073 */ 074public class DialValueIndicator extends AbstractDialLayer implements DialLayer, 075 Cloneable, PublicCloneable, Serializable { 076 077 /** For serialization. */ 078 static final long serialVersionUID = 803094354130942585L; 079 080 /** The dataset index. */ 081 private int datasetIndex; 082 083 /** The angle that defines the anchor point. */ 084 private double angle; 085 086 /** The radius that defines the anchor point. */ 087 private double radius; 088 089 /** The frame anchor. */ 090 private RectangleAnchor frameAnchor; 091 092 /** The template value. */ 093 private Number templateValue; 094 095 /** 096 * A data value that will be formatted to determine the maximum size of 097 * the indicator bounds. If this is null, the indicator bounds can grow 098 * as large as necessary to contain the actual data value. 099 * 100 * @since 1.0.14 101 */ 102 private Number maxTemplateValue; 103 104 /** The formatter. */ 105 private NumberFormat formatter; 106 107 /** The font. */ 108 private Font font; 109 110 /** The paint. */ 111 private transient Paint paint; 112 113 /** The background paint. */ 114 private transient Paint backgroundPaint; 115 116 /** The outline stroke. */ 117 private transient Stroke outlineStroke; 118 119 /** The outline paint. */ 120 private transient Paint outlinePaint; 121 122 /** The insets. */ 123 private RectangleInsets insets; 124 125 /** The value anchor. */ 126 private RectangleAnchor valueAnchor; 127 128 /** The text anchor for displaying the value. */ 129 private TextAnchor textAnchor; 130 131 /** 132 * Creates a new instance of {@code DialValueIndicator}. 133 */ 134 public DialValueIndicator() { 135 this(0); 136 } 137 138 /** 139 * Creates a new instance of {@code DialValueIndicator}. 140 * 141 * @param datasetIndex the dataset index. 142 */ 143 public DialValueIndicator(int datasetIndex) { 144 this.datasetIndex = datasetIndex; 145 this.angle = -90.0; 146 this.radius = 0.3; 147 this.frameAnchor = RectangleAnchor.CENTER; 148 this.templateValue = 100.0; 149 this.maxTemplateValue = null; 150 this.formatter = new DecimalFormat("0.0"); 151 this.font = new Font("Dialog", Font.BOLD, 14); 152 this.paint = Color.BLACK; 153 this.backgroundPaint = Color.WHITE; 154 this.outlineStroke = new BasicStroke(1.0f); 155 this.outlinePaint = Color.BLUE; 156 this.insets = new RectangleInsets(4, 4, 4, 4); 157 this.valueAnchor = RectangleAnchor.RIGHT; 158 this.textAnchor = TextAnchor.CENTER_RIGHT; 159 } 160 161 /** 162 * Returns the index of the dataset from which this indicator fetches its 163 * current value. 164 * 165 * @return The dataset index. 166 * 167 * @see #setDatasetIndex(int) 168 */ 169 public int getDatasetIndex() { 170 return this.datasetIndex; 171 } 172 173 /** 174 * Sets the dataset index and sends a {@link DialLayerChangeEvent} to all 175 * registered listeners. 176 * 177 * @param index the index. 178 * 179 * @see #getDatasetIndex() 180 */ 181 public void setDatasetIndex(int index) { 182 this.datasetIndex = index; 183 notifyListeners(new DialLayerChangeEvent(this)); 184 } 185 186 /** 187 * Returns the angle for the anchor point. The angle is specified in 188 * degrees using the same orientation as Java's {@code Arc2D} class. 189 * 190 * @return The angle (in degrees). 191 * 192 * @see #setAngle(double) 193 */ 194 public double getAngle() { 195 return this.angle; 196 } 197 198 /** 199 * Sets the angle for the anchor point and sends a 200 * {@link DialLayerChangeEvent} to all registered listeners. 201 * 202 * @param angle the angle (in degrees). 203 * 204 * @see #getAngle() 205 */ 206 public void setAngle(double angle) { 207 this.angle = angle; 208 notifyListeners(new DialLayerChangeEvent(this)); 209 } 210 211 /** 212 * Returns the radius. 213 * 214 * @return The radius. 215 * 216 * @see #setRadius(double) 217 */ 218 public double getRadius() { 219 return this.radius; 220 } 221 222 /** 223 * Sets the radius and sends a {@link DialLayerChangeEvent} to all 224 * registered listeners. 225 * 226 * @param radius the radius. 227 * 228 * @see #getRadius() 229 */ 230 public void setRadius(double radius) { 231 this.radius = radius; 232 notifyListeners(new DialLayerChangeEvent(this)); 233 } 234 235 /** 236 * Returns the frame anchor. 237 * 238 * @return The frame anchor. 239 * 240 * @see #setFrameAnchor(RectangleAnchor) 241 */ 242 public RectangleAnchor getFrameAnchor() { 243 return this.frameAnchor; 244 } 245 246 /** 247 * Sets the frame anchor and sends a {@link DialLayerChangeEvent} to all 248 * registered listeners. 249 * 250 * @param anchor the anchor ({@code null} not permitted). 251 * 252 * @see #getFrameAnchor() 253 */ 254 public void setFrameAnchor(RectangleAnchor anchor) { 255 Args.nullNotPermitted(anchor, "anchor"); 256 this.frameAnchor = anchor; 257 notifyListeners(new DialLayerChangeEvent(this)); 258 } 259 260 /** 261 * Returns the template value. 262 * 263 * @return The template value (never {@code null}). 264 * 265 * @see #setTemplateValue(Number) 266 */ 267 public Number getTemplateValue() { 268 return this.templateValue; 269 } 270 271 /** 272 * Sets the template value and sends a {@link DialLayerChangeEvent} to 273 * all registered listeners. 274 * 275 * @param value the value ({@code null} not permitted). 276 * 277 * @see #setTemplateValue(Number) 278 */ 279 public void setTemplateValue(Number value) { 280 Args.nullNotPermitted(value, "value"); 281 this.templateValue = value; 282 notifyListeners(new DialLayerChangeEvent(this)); 283 } 284 285 /** 286 * Returns the template value for the maximum size of the indicator 287 * bounds. 288 * 289 * @return The template value (possibly {@code null}). 290 * 291 * @since 1.0.14 292 * 293 * @see #setMaxTemplateValue(java.lang.Number) 294 */ 295 public Number getMaxTemplateValue() { 296 return this.maxTemplateValue; 297 } 298 299 /** 300 * Sets the template value for the maximum size of the indicator bounds 301 * and sends a {@link DialLayerChangeEvent} to all registered listeners. 302 * 303 * @param value the value ({@code null} permitted). 304 * 305 * @since 1.0.14 306 * 307 * @see #getMaxTemplateValue() 308 */ 309 public void setMaxTemplateValue(Number value) { 310 this.maxTemplateValue = value; 311 notifyListeners(new DialLayerChangeEvent(this)); 312 } 313 314 /** 315 * Returns the formatter used to format the value. 316 * 317 * @return The formatter (never {@code null}). 318 * 319 * @see #setNumberFormat(NumberFormat) 320 */ 321 public NumberFormat getNumberFormat() { 322 return this.formatter; 323 } 324 325 /** 326 * Sets the formatter used to format the value and sends a 327 * {@link DialLayerChangeEvent} to all registered listeners. 328 * 329 * @param formatter the formatter ({@code null} not permitted). 330 * 331 * @see #getNumberFormat() 332 */ 333 public void setNumberFormat(NumberFormat formatter) { 334 Args.nullNotPermitted(formatter, "formatter"); 335 this.formatter = formatter; 336 notifyListeners(new DialLayerChangeEvent(this)); 337 } 338 339 /** 340 * Returns the font. 341 * 342 * @return The font (never {@code null}). 343 * 344 * @see #getFont() 345 */ 346 public Font getFont() { 347 return this.font; 348 } 349 350 /** 351 * Sets the font and sends a {@link DialLayerChangeEvent} to all registered 352 * listeners. 353 * 354 * @param font the font ({@code null} not permitted). 355 */ 356 public void setFont(Font font) { 357 Args.nullNotPermitted(font, "font"); 358 this.font = font; 359 notifyListeners(new DialLayerChangeEvent(this)); 360 } 361 362 /** 363 * Returns the paint. 364 * 365 * @return The paint (never {@code null}). 366 * 367 * @see #setPaint(Paint) 368 */ 369 public Paint getPaint() { 370 return this.paint; 371 } 372 373 /** 374 * Sets the paint and sends a {@link DialLayerChangeEvent} to all 375 * registered listeners. 376 * 377 * @param paint the paint ({@code null} not permitted). 378 * 379 * @see #getPaint() 380 */ 381 public void setPaint(Paint paint) { 382 Args.nullNotPermitted(paint, "paint"); 383 this.paint = paint; 384 notifyListeners(new DialLayerChangeEvent(this)); 385 } 386 387 /** 388 * Returns the background paint. 389 * 390 * @return The background paint. 391 * 392 * @see #setBackgroundPaint(Paint) 393 */ 394 public Paint getBackgroundPaint() { 395 return this.backgroundPaint; 396 } 397 398 /** 399 * Sets the background paint and sends a {@link DialLayerChangeEvent} to 400 * all registered listeners. 401 * 402 * @param paint the paint ({@code null} not permitted). 403 * 404 * @see #getBackgroundPaint() 405 */ 406 public void setBackgroundPaint(Paint paint) { 407 Args.nullNotPermitted(paint, "paint"); 408 this.backgroundPaint = paint; 409 notifyListeners(new DialLayerChangeEvent(this)); 410 } 411 412 /** 413 * Returns the outline stroke. 414 * 415 * @return The outline stroke (never {@code null}). 416 * 417 * @see #setOutlineStroke(Stroke) 418 */ 419 public Stroke getOutlineStroke() { 420 return this.outlineStroke; 421 } 422 423 /** 424 * Sets the outline stroke and sends a {@link DialLayerChangeEvent} to 425 * all registered listeners. 426 * 427 * @param stroke the stroke ({@code null} not permitted). 428 * 429 * @see #getOutlineStroke() 430 */ 431 public void setOutlineStroke(Stroke stroke) { 432 Args.nullNotPermitted(stroke, "stroke"); 433 this.outlineStroke = stroke; 434 notifyListeners(new DialLayerChangeEvent(this)); 435 } 436 437 /** 438 * Returns the outline paint. 439 * 440 * @return The outline paint (never {@code null}). 441 * 442 * @see #setOutlinePaint(Paint) 443 */ 444 public Paint getOutlinePaint() { 445 return this.outlinePaint; 446 } 447 448 /** 449 * Sets the outline paint and sends a {@link DialLayerChangeEvent} to all 450 * registered listeners. 451 * 452 * @param paint the paint ({@code null} not permitted). 453 * 454 * @see #getOutlinePaint() 455 */ 456 public void setOutlinePaint(Paint paint) { 457 Args.nullNotPermitted(paint, "paint"); 458 this.outlinePaint = paint; 459 notifyListeners(new DialLayerChangeEvent(this)); 460 } 461 462 /** 463 * Returns the insets. 464 * 465 * @return The insets (never {@code null}). 466 * 467 * @see #setInsets(RectangleInsets) 468 */ 469 public RectangleInsets getInsets() { 470 return this.insets; 471 } 472 473 /** 474 * Sets the insets and sends a {@link DialLayerChangeEvent} to all 475 * registered listeners. 476 * 477 * @param insets the insets ({@code null} not permitted). 478 * 479 * @see #getInsets() 480 */ 481 public void setInsets(RectangleInsets insets) { 482 Args.nullNotPermitted(insets, "insets"); 483 this.insets = insets; 484 notifyListeners(new DialLayerChangeEvent(this)); 485 } 486 487 /** 488 * Returns the value anchor. 489 * 490 * @return The value anchor (never {@code null}). 491 * 492 * @see #setValueAnchor(RectangleAnchor) 493 */ 494 public RectangleAnchor getValueAnchor() { 495 return this.valueAnchor; 496 } 497 498 /** 499 * Sets the value anchor and sends a {@link DialLayerChangeEvent} to all 500 * registered listeners. 501 * 502 * @param anchor the anchor ({@code null} not permitted). 503 * 504 * @see #getValueAnchor() 505 */ 506 public void setValueAnchor(RectangleAnchor anchor) { 507 Args.nullNotPermitted(anchor, "anchor"); 508 this.valueAnchor = anchor; 509 notifyListeners(new DialLayerChangeEvent(this)); 510 } 511 512 /** 513 * Returns the text anchor. 514 * 515 * @return The text anchor (never {@code null}). 516 * 517 * @see #setTextAnchor(TextAnchor) 518 */ 519 public TextAnchor getTextAnchor() { 520 return this.textAnchor; 521 } 522 523 /** 524 * Sets the text anchor and sends a {@link DialLayerChangeEvent} to all 525 * registered listeners. 526 * 527 * @param anchor the anchor ({@code null} not permitted). 528 * 529 * @see #getTextAnchor() 530 */ 531 public void setTextAnchor(TextAnchor anchor) { 532 Args.nullNotPermitted(anchor, "anchor"); 533 this.textAnchor = anchor; 534 notifyListeners(new DialLayerChangeEvent(this)); 535 } 536 537 /** 538 * Returns {@code true} to indicate that this layer should be 539 * clipped within the dial window. 540 * 541 * @return {@code true}. 542 */ 543 @Override 544 public boolean isClippedToWindow() { 545 return true; 546 } 547 548 /** 549 * Draws the background to the specified graphics device. If the dial 550 * frame specifies a window, the clipping region will already have been 551 * set to this window before this method is called. 552 * 553 * @param g2 the graphics device ({@code null} not permitted). 554 * @param plot the plot (ignored here). 555 * @param frame the dial frame (ignored here). 556 * @param view the view rectangle ({@code null} not permitted). 557 */ 558 @Override 559 public void draw(Graphics2D g2, DialPlot plot, Rectangle2D frame, 560 Rectangle2D view) { 561 562 // work out the anchor point 563 Rectangle2D f = DialPlot.rectangleByRadius(frame, this.radius, 564 this.radius); 565 Arc2D arc = new Arc2D.Double(f, this.angle, 0.0, Arc2D.OPEN); 566 Point2D pt = arc.getStartPoint(); 567 568 // the indicator bounds is calculated from the templateValue (which 569 // determines the minimum size), the maxTemplateValue (which, if 570 // specified, provides a maximum size) and the actual value 571 FontMetrics fm = g2.getFontMetrics(this.font); 572 double value = plot.getValue(this.datasetIndex); 573 String valueStr = this.formatter.format(value); 574 Rectangle2D valueBounds = TextUtils.getTextBounds(valueStr, g2, fm); 575 576 // calculate the bounds of the template value 577 String s = this.formatter.format(this.templateValue); 578 Rectangle2D tb = TextUtils.getTextBounds(s, g2, fm); 579 double minW = tb.getWidth(); 580 double minH = tb.getHeight(); 581 582 double maxW = Double.MAX_VALUE; 583 double maxH = Double.MAX_VALUE; 584 if (this.maxTemplateValue != null) { 585 s = this.formatter.format(this.maxTemplateValue); 586 tb = TextUtils.getTextBounds(s, g2, fm); 587 maxW = Math.max(tb.getWidth(), minW); 588 maxH = Math.max(tb.getHeight(), minH); 589 } 590 double w = fixToRange(valueBounds.getWidth(), minW, maxW); 591 double h = fixToRange(valueBounds.getHeight(), minH, maxH); 592 593 // align this rectangle to the frameAnchor 594 Rectangle2D bounds = RectangleAnchor.createRectangle(new Size2D(w, h), 595 pt.getX(), pt.getY(), this.frameAnchor); 596 597 // add the insets 598 Rectangle2D fb = this.insets.createOutsetRectangle(bounds); 599 600 // draw the background 601 g2.setPaint(this.backgroundPaint); 602 g2.fill(fb); 603 604 // draw the border 605 g2.setStroke(this.outlineStroke); 606 g2.setPaint(this.outlinePaint); 607 g2.draw(fb); 608 609 // now find the text anchor point 610 Shape savedClip = g2.getClip(); 611 g2.clip(fb); 612 613 Point2D pt2 = this.valueAnchor.getAnchorPoint(bounds); 614 g2.setPaint(this.paint); 615 g2.setFont(this.font); 616 TextUtils.drawAlignedString(valueStr, g2, (float) pt2.getX(), 617 (float) pt2.getY(), this.textAnchor); 618 g2.setClip(savedClip); 619 620 } 621 622 /** 623 * A utility method that adjusts a value, if necessary, to be within a 624 * specified range. 625 * 626 * @param x the value. 627 * @param minX the minimum value in the range. 628 * @param maxX the maximum value in the range. 629 * 630 * @return The adjusted value. 631 */ 632 private double fixToRange(double x, double minX, double maxX) { 633 if (minX > maxX) { 634 throw new IllegalArgumentException("Requires 'minX' <= 'maxX'."); 635 } 636 if (x < minX) { 637 return minX; 638 } 639 else if (x > maxX) { 640 return maxX; 641 } 642 else { 643 return x; 644 } 645 } 646 647 /** 648 * Tests this instance for equality with an arbitrary object. 649 * 650 * @param obj the object ({@code null} permitted). 651 * 652 * @return A boolean. 653 */ 654 @Override 655 public boolean equals(Object obj) { 656 if (obj == this) { 657 return true; 658 } 659 if (!(obj instanceof DialValueIndicator)) { 660 return false; 661 } 662 DialValueIndicator that = (DialValueIndicator) obj; 663 if (this.datasetIndex != that.datasetIndex) { 664 return false; 665 } 666 if (this.angle != that.angle) { 667 return false; 668 } 669 if (this.radius != that.radius) { 670 return false; 671 } 672 if (!this.frameAnchor.equals(that.frameAnchor)) { 673 return false; 674 } 675 if (!this.templateValue.equals(that.templateValue)) { 676 return false; 677 } 678 if (!ObjectUtils.equal(this.maxTemplateValue, 679 that.maxTemplateValue)) { 680 return false; 681 } 682 if (!this.font.equals(that.font)) { 683 return false; 684 } 685 if (!PaintUtils.equal(this.paint, that.paint)) { 686 return false; 687 } 688 if (!PaintUtils.equal(this.backgroundPaint, that.backgroundPaint)) { 689 return false; 690 } 691 if (!this.outlineStroke.equals(that.outlineStroke)) { 692 return false; 693 } 694 if (!PaintUtils.equal(this.outlinePaint, that.outlinePaint)) { 695 return false; 696 } 697 if (!this.insets.equals(that.insets)) { 698 return false; 699 } 700 if (!this.valueAnchor.equals(that.valueAnchor)) { 701 return false; 702 } 703 if (!this.textAnchor.equals(that.textAnchor)) { 704 return false; 705 } 706 return super.equals(obj); 707 } 708 709 /** 710 * Returns a hash code for this instance. 711 * 712 * @return The hash code. 713 */ 714 @Override 715 public int hashCode() { 716 int result = 193; 717 result = 37 * result + HashUtils.hashCodeForPaint(this.paint); 718 result = 37 * result + HashUtils.hashCodeForPaint( 719 this.backgroundPaint); 720 result = 37 * result + HashUtils.hashCodeForPaint( 721 this.outlinePaint); 722 result = 37 * result + this.outlineStroke.hashCode(); 723 return result; 724 } 725 726 /** 727 * Returns a clone of this instance. 728 * 729 * @return The clone. 730 * 731 * @throws CloneNotSupportedException if some attribute of this instance 732 * cannot be cloned. 733 */ 734 @Override 735 public Object clone() throws CloneNotSupportedException { 736 return super.clone(); 737 } 738 739 /** 740 * Provides serialization support. 741 * 742 * @param stream the output stream. 743 * 744 * @throws IOException if there is an I/O error. 745 */ 746 private void writeObject(ObjectOutputStream stream) throws IOException { 747 stream.defaultWriteObject(); 748 SerialUtils.writePaint(this.paint, stream); 749 SerialUtils.writePaint(this.backgroundPaint, stream); 750 SerialUtils.writePaint(this.outlinePaint, stream); 751 SerialUtils.writeStroke(this.outlineStroke, stream); 752 } 753 754 /** 755 * Provides serialization support. 756 * 757 * @param stream the input stream. 758 * 759 * @throws IOException if there is an I/O error. 760 * @throws ClassNotFoundException if there is a classpath problem. 761 */ 762 private void readObject(ObjectInputStream stream) 763 throws IOException, ClassNotFoundException { 764 stream.defaultReadObject(); 765 this.paint = SerialUtils.readPaint(stream); 766 this.backgroundPaint = SerialUtils.readPaint(stream); 767 this.outlinePaint = SerialUtils.readPaint(stream); 768 this.outlineStroke = SerialUtils.readStroke(stream); 769 } 770 771}