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