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