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