001 /* =========================================================== 002 * JFreeChart : a free chart library for the Java(tm) platform 003 * =========================================================== 004 * 005 * (C) Copyright 2000-2007, 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 * [Java is a trademark or registered trademark of Sun Microsystems, Inc. 025 * in the United States and other countries.] 026 * 027 * -------------------- 028 * ThermometerPlot.java 029 * -------------------- 030 * 031 * (C) Copyright 2000-2007, by Bryan Scott and Contributors. 032 * 033 * Original Author: Bryan Scott (based on MeterPlot by Hari). 034 * Contributor(s): David Gilbert (for Object Refinery Limited). 035 * Arnaud Lelievre; 036 * 037 * Changes 038 * ------- 039 * 11-Apr-2002 : Version 1, contributed by Bryan Scott; 040 * 15-Apr-2002 : Changed to implement VerticalValuePlot; 041 * 29-Apr-2002 : Added getVerticalValueAxis() method (DG); 042 * 25-Jun-2002 : Removed redundant imports (DG); 043 * 17-Sep-2002 : Reviewed with Checkstyle utility (DG); 044 * 18-Sep-2002 : Extensive changes made to API, to iron out bugs and 045 * inconsistencies (DG); 046 * 13-Oct-2002 : Corrected error datasetChanged which would generate exceptions 047 * when value set to null (BRS). 048 * 23-Jan-2003 : Removed one constructor (DG); 049 * 26-Mar-2003 : Implemented Serializable (DG); 050 * 02-Jun-2003 : Removed test for compatible range axis (DG); 051 * 01-Jul-2003 : Added additional check in draw method to ensure value not 052 * null (BRS); 053 * 08-Sep-2003 : Added internationalization via use of properties 054 * resourceBundle (RFE 690236) (AL); 055 * 16-Sep-2003 : Changed ChartRenderingInfo --> PlotRenderingInfo (DG); 056 * 29-Sep-2003 : Updated draw to set value of cursor to non-zero and allow 057 * painting of axis. An incomplete fix and needs to be set for 058 * left or right drawing (BRS); 059 * 19-Nov-2003 : Added support for value labels to be displayed left of the 060 * thermometer 061 * 19-Nov-2003 : Improved axis drawing (now default axis does not draw axis line 062 * and is closer to the bulb). Added support for the positioning 063 * of the axis to the left or right of the bulb. (BRS); 064 * 03-Dec-2003 : Directly mapped deprecated setData()/getData() method to 065 * get/setDataset() (TM); 066 * 21-Jan-2004 : Update for renamed method in ValueAxis (DG); 067 * 07-Apr-2004 : Changed string width calculation (DG); 068 * 12-Nov-2004 : Implemented the new Zoomable interface (DG); 069 * 06-Jan-2004 : Added getOrientation() method (DG); 070 * 11-Jan-2005 : Removed deprecated code in preparation for 1.0.0 release (DG); 071 * 29-Mar-2005 : Fixed equals() method (DG); 072 * 05-May-2005 : Updated draw() method parameters (DG); 073 * 09-Jun-2005 : Fixed more bugs in equals() method (DG); 074 * 10-Jun-2005 : Fixed minor bug in setDisplayRange() method (DG); 075 * ------------- JFREECHART 1.0.x --------------------------------------------- 076 * 14-Nov-2006 : Fixed margin when drawing (DG); 077 * 078 */ 079 080 package org.jfree.chart.plot; 081 082 import java.awt.BasicStroke; 083 import java.awt.Color; 084 import java.awt.Font; 085 import java.awt.FontMetrics; 086 import java.awt.Graphics2D; 087 import java.awt.Paint; 088 import java.awt.Stroke; 089 import java.awt.geom.Area; 090 import java.awt.geom.Ellipse2D; 091 import java.awt.geom.Line2D; 092 import java.awt.geom.Point2D; 093 import java.awt.geom.Rectangle2D; 094 import java.awt.geom.RoundRectangle2D; 095 import java.io.IOException; 096 import java.io.ObjectInputStream; 097 import java.io.ObjectOutputStream; 098 import java.io.Serializable; 099 import java.text.DecimalFormat; 100 import java.text.NumberFormat; 101 import java.util.Arrays; 102 import java.util.ResourceBundle; 103 104 import org.jfree.chart.LegendItemCollection; 105 import org.jfree.chart.axis.NumberAxis; 106 import org.jfree.chart.axis.ValueAxis; 107 import org.jfree.chart.event.PlotChangeEvent; 108 import org.jfree.data.Range; 109 import org.jfree.data.general.DatasetChangeEvent; 110 import org.jfree.data.general.DefaultValueDataset; 111 import org.jfree.data.general.ValueDataset; 112 import org.jfree.io.SerialUtilities; 113 import org.jfree.ui.RectangleEdge; 114 import org.jfree.ui.RectangleInsets; 115 import org.jfree.util.ObjectUtilities; 116 import org.jfree.util.PaintUtilities; 117 import org.jfree.util.UnitType; 118 119 /** 120 * A plot that displays a single value (from a {@link ValueDataset}) in a 121 * thermometer type display. 122 * <p> 123 * This plot supports a number of options: 124 * <ol> 125 * <li>three sub-ranges which could be viewed as 'Normal', 'Warning' 126 * and 'Critical' ranges.</li> 127 * <li>the thermometer can be run in two modes: 128 * <ul> 129 * <li>fixed range, or</li> 130 * <li>range adjusts to current sub-range.</li> 131 * </ul> 132 * </li> 133 * <li>settable units to be displayed.</li> 134 * <li>settable display location for the value text.</li> 135 * </ol> 136 */ 137 public class ThermometerPlot extends Plot implements ValueAxisPlot, 138 Zoomable, 139 Cloneable, 140 Serializable { 141 142 /** For serialization. */ 143 private static final long serialVersionUID = 4087093313147984390L; 144 145 /** A constant for unit type 'None'. */ 146 public static final int UNITS_NONE = 0; 147 148 /** A constant for unit type 'Fahrenheit'. */ 149 public static final int UNITS_FAHRENHEIT = 1; 150 151 /** A constant for unit type 'Celcius'. */ 152 public static final int UNITS_CELCIUS = 2; 153 154 /** A constant for unit type 'Kelvin'. */ 155 public static final int UNITS_KELVIN = 3; 156 157 /** A constant for the value label position (no label). */ 158 public static final int NONE = 0; 159 160 /** A constant for the value label position (right of the thermometer). */ 161 public static final int RIGHT = 1; 162 163 /** A constant for the value label position (left of the thermometer). */ 164 public static final int LEFT = 2; 165 166 /** A constant for the value label position (in the thermometer bulb). */ 167 public static final int BULB = 3; 168 169 /** A constant for the 'normal' range. */ 170 public static final int NORMAL = 0; 171 172 /** A constant for the 'warning' range. */ 173 public static final int WARNING = 1; 174 175 /** A constant for the 'critical' range. */ 176 public static final int CRITICAL = 2; 177 178 /** The bulb radius. */ 179 protected static final int BULB_RADIUS = 40; 180 181 /** The bulb diameter. */ 182 protected static final int BULB_DIAMETER = BULB_RADIUS * 2; 183 184 /** The column radius. */ 185 protected static final int COLUMN_RADIUS = 20; 186 187 /** The column diameter.*/ 188 protected static final int COLUMN_DIAMETER = COLUMN_RADIUS * 2; 189 190 /** The gap radius. */ 191 protected static final int GAP_RADIUS = 5; 192 193 /** The gap diameter. */ 194 protected static final int GAP_DIAMETER = GAP_RADIUS * 2; 195 196 /** The axis gap. */ 197 protected static final int AXIS_GAP = 10; 198 199 /** The unit strings. */ 200 protected static final String[] UNITS 201 = {"", "\u00B0F", "\u00B0C", "\u00B0K"}; 202 203 /** Index for low value in subrangeInfo matrix. */ 204 protected static final int RANGE_LOW = 0; 205 206 /** Index for high value in subrangeInfo matrix. */ 207 protected static final int RANGE_HIGH = 1; 208 209 /** Index for display low value in subrangeInfo matrix. */ 210 protected static final int DISPLAY_LOW = 2; 211 212 /** Index for display high value in subrangeInfo matrix. */ 213 protected static final int DISPLAY_HIGH = 3; 214 215 /** The default lower bound. */ 216 protected static final double DEFAULT_LOWER_BOUND = 0.0; 217 218 /** The default upper bound. */ 219 protected static final double DEFAULT_UPPER_BOUND = 100.0; 220 221 /** The dataset for the plot. */ 222 private ValueDataset dataset; 223 224 /** The range axis. */ 225 private ValueAxis rangeAxis; 226 227 /** The lower bound for the thermometer. */ 228 private double lowerBound = DEFAULT_LOWER_BOUND; 229 230 /** The upper bound for the thermometer. */ 231 private double upperBound = DEFAULT_UPPER_BOUND; 232 233 /** 234 * Blank space inside the plot area around the outside of the thermometer. 235 */ 236 private RectangleInsets padding; 237 238 /** Stroke for drawing the thermometer */ 239 private transient Stroke thermometerStroke = new BasicStroke(1.0f); 240 241 /** Paint for drawing the thermometer */ 242 private transient Paint thermometerPaint = Color.black; 243 244 /** The display units */ 245 private int units = UNITS_CELCIUS; 246 247 /** The value label position. */ 248 private int valueLocation = BULB; 249 250 /** The position of the axis **/ 251 private int axisLocation = LEFT; 252 253 /** The font to write the value in */ 254 private Font valueFont = new Font("SansSerif", Font.BOLD, 16); 255 256 /** Colour that the value is written in */ 257 private transient Paint valuePaint = Color.white; 258 259 /** Number format for the value */ 260 private NumberFormat valueFormat = new DecimalFormat(); 261 262 /** The default paint for the mercury in the thermometer. */ 263 private transient Paint mercuryPaint = Color.lightGray; 264 265 /** A flag that controls whether value lines are drawn. */ 266 private boolean showValueLines = false; 267 268 /** The display sub-range. */ 269 private int subrange = -1; 270 271 /** The start and end values for the subranges. */ 272 private double[][] subrangeInfo = { 273 {0.0, 50.0, 0.0, 50.0}, 274 {50.0, 75.0, 50.0, 75.0}, 275 {75.0, 100.0, 75.0, 100.0} 276 }; 277 278 /** 279 * A flag that controls whether or not the axis range adjusts to the 280 * sub-ranges. 281 */ 282 private boolean followDataInSubranges = false; 283 284 /** 285 * A flag that controls whether or not the mercury paint changes with 286 * the subranges. 287 */ 288 private boolean useSubrangePaint = true; 289 290 /** Paint for each range */ 291 private Paint[] subrangePaint = { 292 Color.green, 293 Color.orange, 294 Color.red 295 }; 296 297 /** A flag that controls whether the sub-range indicators are visible. */ 298 private boolean subrangeIndicatorsVisible = true; 299 300 /** The stroke for the sub-range indicators. */ 301 private transient Stroke subrangeIndicatorStroke = new BasicStroke(2.0f); 302 303 /** The range indicator stroke. */ 304 private transient Stroke rangeIndicatorStroke = new BasicStroke(3.0f); 305 306 /** The resourceBundle for the localization. */ 307 protected static ResourceBundle localizationResources = 308 ResourceBundle.getBundle("org.jfree.chart.plot.LocalizationBundle"); 309 310 /** 311 * Creates a new thermometer plot. 312 */ 313 public ThermometerPlot() { 314 this(new DefaultValueDataset()); 315 } 316 317 /** 318 * Creates a new thermometer plot, using default attributes where necessary. 319 * 320 * @param dataset the data set. 321 */ 322 public ThermometerPlot(ValueDataset dataset) { 323 324 super(); 325 326 this.padding = new RectangleInsets(UnitType.RELATIVE, 0.05, 0.05, 0.05, 327 0.05); 328 this.dataset = dataset; 329 if (dataset != null) { 330 dataset.addChangeListener(this); 331 } 332 NumberAxis axis = new NumberAxis(null); 333 axis.setStandardTickUnits(NumberAxis.createIntegerTickUnits()); 334 axis.setAxisLineVisible(false); 335 336 setRangeAxis(axis); 337 setAxisRange(); 338 } 339 340 /** 341 * Returns the primary dataset for the plot. 342 * 343 * @return The primary dataset (possibly <code>null</code>). 344 */ 345 public ValueDataset getDataset() { 346 return this.dataset; 347 } 348 349 /** 350 * Sets the dataset for the plot, replacing the existing dataset if there 351 * is one. 352 * 353 * @param dataset the dataset (<code>null</code> permitted). 354 */ 355 public void setDataset(ValueDataset dataset) { 356 357 // if there is an existing dataset, remove the plot from the list 358 // of change listeners... 359 ValueDataset existing = this.dataset; 360 if (existing != null) { 361 existing.removeChangeListener(this); 362 } 363 364 // set the new dataset, and register the chart as a change listener... 365 this.dataset = dataset; 366 if (dataset != null) { 367 setDatasetGroup(dataset.getGroup()); 368 dataset.addChangeListener(this); 369 } 370 371 // send a dataset change event to self... 372 DatasetChangeEvent event = new DatasetChangeEvent(this, dataset); 373 datasetChanged(event); 374 375 } 376 377 /** 378 * Returns the range axis. 379 * 380 * @return The range axis. 381 */ 382 public ValueAxis getRangeAxis() { 383 return this.rangeAxis; 384 } 385 386 /** 387 * Sets the range axis for the plot. 388 * 389 * @param axis the new axis. 390 */ 391 public void setRangeAxis(ValueAxis axis) { 392 393 if (axis != null) { 394 axis.setPlot(this); 395 axis.addChangeListener(this); 396 } 397 398 // plot is likely registered as a listener with the existing axis... 399 if (this.rangeAxis != null) { 400 this.rangeAxis.removeChangeListener(this); 401 } 402 403 this.rangeAxis = axis; 404 405 } 406 407 /** 408 * Returns the lower bound for the thermometer. The data value can be set 409 * lower than this, but it will not be shown in the thermometer. 410 * 411 * @return The lower bound. 412 * 413 */ 414 public double getLowerBound() { 415 return this.lowerBound; 416 } 417 418 /** 419 * Sets the lower bound for the thermometer. 420 * 421 * @param lower the lower bound. 422 */ 423 public void setLowerBound(double lower) { 424 this.lowerBound = lower; 425 setAxisRange(); 426 } 427 428 /** 429 * Returns the upper bound for the thermometer. The data value can be set 430 * higher than this, but it will not be shown in the thermometer. 431 * 432 * @return The upper bound. 433 */ 434 public double getUpperBound() { 435 return this.upperBound; 436 } 437 438 /** 439 * Sets the upper bound for the thermometer. 440 * 441 * @param upper the upper bound. 442 */ 443 public void setUpperBound(double upper) { 444 this.upperBound = upper; 445 setAxisRange(); 446 } 447 448 /** 449 * Sets the lower and upper bounds for the thermometer. 450 * 451 * @param lower the lower bound. 452 * @param upper the upper bound. 453 */ 454 public void setRange(double lower, double upper) { 455 this.lowerBound = lower; 456 this.upperBound = upper; 457 setAxisRange(); 458 } 459 460 /** 461 * Returns the padding for the thermometer. This is the space inside the 462 * plot area. 463 * 464 * @return The padding. 465 */ 466 public RectangleInsets getPadding() { 467 return this.padding; 468 } 469 470 /** 471 * Sets the padding for the thermometer. 472 * 473 * @param padding the padding. 474 */ 475 public void setPadding(RectangleInsets padding) { 476 this.padding = padding; 477 notifyListeners(new PlotChangeEvent(this)); 478 } 479 480 /** 481 * Returns the stroke used to draw the thermometer outline. 482 * 483 * @return The stroke. 484 */ 485 public Stroke getThermometerStroke() { 486 return this.thermometerStroke; 487 } 488 489 /** 490 * Sets the stroke used to draw the thermometer outline. 491 * 492 * @param s the new stroke (null ignored). 493 */ 494 public void setThermometerStroke(Stroke s) { 495 if (s != null) { 496 this.thermometerStroke = s; 497 notifyListeners(new PlotChangeEvent(this)); 498 } 499 } 500 501 /** 502 * Returns the paint used to draw the thermometer outline. 503 * 504 * @return The paint. 505 */ 506 public Paint getThermometerPaint() { 507 return this.thermometerPaint; 508 } 509 510 /** 511 * Sets the paint used to draw the thermometer outline. 512 * 513 * @param paint the new paint (null ignored). 514 */ 515 public void setThermometerPaint(Paint paint) { 516 if (paint != null) { 517 this.thermometerPaint = paint; 518 notifyListeners(new PlotChangeEvent(this)); 519 } 520 } 521 522 /** 523 * Returns the unit display type (none/Fahrenheit/Celcius/Kelvin). 524 * 525 * @return The units type. 526 */ 527 public int getUnits() { 528 return this.units; 529 } 530 531 /** 532 * Sets the units to be displayed in the thermometer. 533 * <p> 534 * Use one of the following constants: 535 * 536 * <ul> 537 * <li>UNITS_NONE : no units displayed.</li> 538 * <li>UNITS_FAHRENHEIT : units displayed in Fahrenheit.</li> 539 * <li>UNITS_CELCIUS : units displayed in Celcius.</li> 540 * <li>UNITS_KELVIN : units displayed in Kelvin.</li> 541 * </ul> 542 * 543 * @param u the new unit type. 544 */ 545 public void setUnits(int u) { 546 if ((u >= 0) && (u < UNITS.length)) { 547 if (this.units != u) { 548 this.units = u; 549 notifyListeners(new PlotChangeEvent(this)); 550 } 551 } 552 } 553 554 /** 555 * Sets the unit type. 556 * 557 * @param u the unit type (null ignored). 558 */ 559 public void setUnits(String u) { 560 if (u == null) { 561 return; 562 } 563 564 u = u.toUpperCase().trim(); 565 for (int i = 0; i < UNITS.length; ++i) { 566 if (u.equals(UNITS[i].toUpperCase().trim())) { 567 setUnits(i); 568 i = UNITS.length; 569 } 570 } 571 } 572 573 /** 574 * Returns the value location. 575 * 576 * @return The location. 577 */ 578 public int getValueLocation() { 579 return this.valueLocation; 580 } 581 582 /** 583 * Sets the location at which the current value is displayed. 584 * <P> 585 * The location can be one of the constants: 586 * <code>NONE</code>, 587 * <code>RIGHT</code> 588 * <code>LEFT</code> and 589 * <code>BULB</code>. 590 * 591 * @param location the location. 592 */ 593 public void setValueLocation(int location) { 594 if ((location >= 0) && (location < 4)) { 595 this.valueLocation = location; 596 notifyListeners(new PlotChangeEvent(this)); 597 } 598 else { 599 throw new IllegalArgumentException("Location not recognised."); 600 } 601 } 602 603 /** 604 * Sets the location at which the axis is displayed with reference to the 605 * bulb. 606 * <P> 607 * The location can be one of the constants: 608 * <code>NONE</code>, 609 * <code>RIGHT</code> and 610 * <code>LEFT</code>. 611 * 612 * @param location the location. 613 */ 614 public void setAxisLocation(int location) { 615 if ((location >= 0) && (location < 3)) { 616 this.axisLocation = location; 617 notifyListeners(new PlotChangeEvent(this)); 618 } 619 else { 620 throw new IllegalArgumentException("Location not recognised."); 621 } 622 } 623 624 /** 625 * Returns the axis location. 626 * 627 * @return The location. 628 */ 629 public int getAxisLocation() { 630 return this.axisLocation; 631 } 632 633 /** 634 * Gets the font used to display the current value. 635 * 636 * @return The font. 637 */ 638 public Font getValueFont() { 639 return this.valueFont; 640 } 641 642 /** 643 * Sets the font used to display the current value. 644 * 645 * @param f the new font. 646 */ 647 public void setValueFont(Font f) { 648 if ((f != null) && (!this.valueFont.equals(f))) { 649 this.valueFont = f; 650 notifyListeners(new PlotChangeEvent(this)); 651 } 652 } 653 654 /** 655 * Gets the paint used to display the current value. 656 * 657 * @return The paint. 658 */ 659 public Paint getValuePaint() { 660 return this.valuePaint; 661 } 662 663 /** 664 * Sets the paint used to display the current value. 665 * 666 * @param p the new paint. 667 */ 668 public void setValuePaint(Paint p) { 669 if ((p != null) && (!this.valuePaint.equals(p))) { 670 this.valuePaint = p; 671 notifyListeners(new PlotChangeEvent(this)); 672 } 673 } 674 675 /** 676 * Sets the formatter for the value label. 677 * 678 * @param formatter the new formatter. 679 */ 680 public void setValueFormat(NumberFormat formatter) { 681 if (formatter != null) { 682 this.valueFormat = formatter; 683 notifyListeners(new PlotChangeEvent(this)); 684 } 685 } 686 687 /** 688 * Returns the default mercury paint. 689 * 690 * @return The paint. 691 */ 692 public Paint getMercuryPaint() { 693 return this.mercuryPaint; 694 } 695 696 /** 697 * Sets the default mercury paint. 698 * 699 * @param paint the new paint. 700 */ 701 public void setMercuryPaint(Paint paint) { 702 this.mercuryPaint = paint; 703 notifyListeners(new PlotChangeEvent(this)); 704 } 705 706 /** 707 * Returns the flag that controls whether not value lines are displayed. 708 * 709 * @return The flag. 710 */ 711 public boolean getShowValueLines() { 712 return this.showValueLines; 713 } 714 715 /** 716 * Sets the display as to whether to show value lines in the output. 717 * 718 * @param b Whether to show value lines in the thermometer 719 */ 720 public void setShowValueLines(boolean b) { 721 this.showValueLines = b; 722 notifyListeners(new PlotChangeEvent(this)); 723 } 724 725 /** 726 * Sets information for a particular range. 727 * 728 * @param range the range to specify information about. 729 * @param low the low value for the range 730 * @param hi the high value for the range 731 */ 732 public void setSubrangeInfo(int range, double low, double hi) { 733 setSubrangeInfo(range, low, hi, low, hi); 734 } 735 736 /** 737 * Sets the subrangeInfo attribute of the ThermometerPlot object 738 * 739 * @param range the new rangeInfo value. 740 * @param rangeLow the new rangeInfo value 741 * @param rangeHigh the new rangeInfo value 742 * @param displayLow the new rangeInfo value 743 * @param displayHigh the new rangeInfo value 744 */ 745 public void setSubrangeInfo(int range, 746 double rangeLow, double rangeHigh, 747 double displayLow, double displayHigh) { 748 749 if ((range >= 0) && (range < 3)) { 750 setSubrange(range, rangeLow, rangeHigh); 751 setDisplayRange(range, displayLow, displayHigh); 752 setAxisRange(); 753 notifyListeners(new PlotChangeEvent(this)); 754 } 755 756 } 757 758 /** 759 * Sets the range. 760 * 761 * @param range the range type. 762 * @param low the low value. 763 * @param high the high value. 764 */ 765 public void setSubrange(int range, double low, double high) { 766 if ((range >= 0) && (range < 3)) { 767 this.subrangeInfo[range][RANGE_HIGH] = high; 768 this.subrangeInfo[range][RANGE_LOW] = low; 769 } 770 } 771 772 /** 773 * Sets the display range. 774 * 775 * @param range the range type. 776 * @param low the low value. 777 * @param high the high value. 778 */ 779 public void setDisplayRange(int range, double low, double high) { 780 781 if ((range >= 0) && (range < this.subrangeInfo.length) 782 && isValidNumber(high) && isValidNumber(low)) { 783 784 if (high > low) { 785 this.subrangeInfo[range][DISPLAY_HIGH] = high; 786 this.subrangeInfo[range][DISPLAY_LOW] = low; 787 } 788 else { 789 this.subrangeInfo[range][DISPLAY_HIGH] = low; 790 this.subrangeInfo[range][DISPLAY_LOW] = high; 791 } 792 793 } 794 795 } 796 797 /** 798 * Gets the paint used for a particular subrange. 799 * 800 * @param range the range. 801 * 802 * @return The paint. 803 */ 804 public Paint getSubrangePaint(int range) { 805 if ((range >= 0) && (range < this.subrangePaint.length)) { 806 return this.subrangePaint[range]; 807 } 808 else { 809 return this.mercuryPaint; 810 } 811 } 812 813 /** 814 * Sets the paint to be used for a range. 815 * 816 * @param range the range. 817 * @param paint the paint to be applied. 818 */ 819 public void setSubrangePaint(int range, Paint paint) { 820 if ((range >= 0) 821 && (range < this.subrangePaint.length) && (paint != null)) { 822 this.subrangePaint[range] = paint; 823 notifyListeners(new PlotChangeEvent(this)); 824 } 825 } 826 827 /** 828 * Returns a flag that controls whether or not the thermometer axis zooms 829 * to display the subrange within which the data value falls. 830 * 831 * @return The flag. 832 */ 833 public boolean getFollowDataInSubranges() { 834 return this.followDataInSubranges; 835 } 836 837 /** 838 * Sets the flag that controls whether or not the thermometer axis zooms 839 * to display the subrange within which the data value falls. 840 * 841 * @param flag the flag. 842 */ 843 public void setFollowDataInSubranges(boolean flag) { 844 this.followDataInSubranges = flag; 845 notifyListeners(new PlotChangeEvent(this)); 846 } 847 848 /** 849 * Returns a flag that controls whether or not the mercury color changes 850 * for each subrange. 851 * 852 * @return The flag. 853 */ 854 public boolean getUseSubrangePaint() { 855 return this.useSubrangePaint; 856 } 857 858 /** 859 * Sets the range colour change option. 860 * 861 * @param flag The new range colour change option 862 */ 863 public void setUseSubrangePaint(boolean flag) { 864 this.useSubrangePaint = flag; 865 notifyListeners(new PlotChangeEvent(this)); 866 } 867 868 /** 869 * Draws the plot on a Java 2D graphics device (such as the screen or a 870 * printer). 871 * 872 * @param g2 the graphics device. 873 * @param area the area within which the plot should be drawn. 874 * @param anchor the anchor point (<code>null</code> permitted). 875 * @param parentState the state from the parent plot, if there is one. 876 * @param info collects info about the drawing. 877 */ 878 public void draw(Graphics2D g2, Rectangle2D area, Point2D anchor, 879 PlotState parentState, 880 PlotRenderingInfo info) { 881 882 RoundRectangle2D outerStem = new RoundRectangle2D.Double(); 883 RoundRectangle2D innerStem = new RoundRectangle2D.Double(); 884 RoundRectangle2D mercuryStem = new RoundRectangle2D.Double(); 885 Ellipse2D outerBulb = new Ellipse2D.Double(); 886 Ellipse2D innerBulb = new Ellipse2D.Double(); 887 String temp = null; 888 FontMetrics metrics = null; 889 if (info != null) { 890 info.setPlotArea(area); 891 } 892 893 // adjust for insets... 894 RectangleInsets insets = getInsets(); 895 insets.trim(area); 896 drawBackground(g2, area); 897 898 // adjust for padding... 899 Rectangle2D interior = (Rectangle2D) area.clone(); 900 this.padding.trim(interior); 901 int midX = (int) (interior.getX() + (interior.getWidth() / 2)); 902 int midY = (int) (interior.getY() + (interior.getHeight() / 2)); 903 int stemTop = (int) (interior.getMinY() + BULB_RADIUS); 904 int stemBottom = (int) (interior.getMaxY() - BULB_DIAMETER); 905 Rectangle2D dataArea = new Rectangle2D.Double(midX - COLUMN_RADIUS, 906 stemTop, COLUMN_RADIUS, stemBottom - stemTop); 907 908 outerBulb.setFrame(midX - BULB_RADIUS, stemBottom, BULB_DIAMETER, 909 BULB_DIAMETER); 910 911 outerStem.setRoundRect(midX - COLUMN_RADIUS, interior.getMinY(), 912 COLUMN_DIAMETER, stemBottom + BULB_DIAMETER - stemTop, 913 COLUMN_DIAMETER, COLUMN_DIAMETER); 914 915 Area outerThermometer = new Area(outerBulb); 916 Area tempArea = new Area(outerStem); 917 outerThermometer.add(tempArea); 918 919 innerBulb.setFrame(midX - BULB_RADIUS + GAP_RADIUS, 920 stemBottom + GAP_RADIUS, BULB_DIAMETER - GAP_DIAMETER, 921 BULB_DIAMETER - GAP_DIAMETER); 922 923 innerStem.setRoundRect(midX - COLUMN_RADIUS + GAP_RADIUS, 924 interior.getMinY() + GAP_RADIUS, COLUMN_DIAMETER - GAP_DIAMETER, 925 stemBottom + BULB_DIAMETER - GAP_DIAMETER - stemTop, 926 COLUMN_DIAMETER - GAP_DIAMETER, COLUMN_DIAMETER - GAP_DIAMETER); 927 928 Area innerThermometer = new Area(innerBulb); 929 tempArea = new Area(innerStem); 930 innerThermometer.add(tempArea); 931 932 if ((this.dataset != null) && (this.dataset.getValue() != null)) { 933 double current = this.dataset.getValue().doubleValue(); 934 double ds = this.rangeAxis.valueToJava2D(current, dataArea, 935 RectangleEdge.LEFT); 936 937 int i = COLUMN_DIAMETER - GAP_DIAMETER; // already calculated 938 int j = COLUMN_RADIUS - GAP_RADIUS; // already calculated 939 int l = (i / 2); 940 int k = (int) Math.round(ds); 941 if (k < (GAP_RADIUS + interior.getMinY())) { 942 k = (int) (GAP_RADIUS + interior.getMinY()); 943 l = BULB_RADIUS; 944 } 945 946 Area mercury = new Area(innerBulb); 947 948 if (k < (stemBottom + BULB_RADIUS)) { 949 mercuryStem.setRoundRect(midX - j, k, i, 950 (stemBottom + BULB_RADIUS) - k, l, l); 951 tempArea = new Area(mercuryStem); 952 mercury.add(tempArea); 953 } 954 955 g2.setPaint(getCurrentPaint()); 956 g2.fill(mercury); 957 958 // draw range indicators... 959 if (this.subrangeIndicatorsVisible) { 960 g2.setStroke(this.subrangeIndicatorStroke); 961 Range range = this.rangeAxis.getRange(); 962 963 // draw start of normal range 964 double value = this.subrangeInfo[NORMAL][RANGE_LOW]; 965 if (range.contains(value)) { 966 double x = midX + COLUMN_RADIUS + 2; 967 double y = this.rangeAxis.valueToJava2D(value, dataArea, 968 RectangleEdge.LEFT); 969 Line2D line = new Line2D.Double(x, y, x + 10, y); 970 g2.setPaint(this.subrangePaint[NORMAL]); 971 g2.draw(line); 972 } 973 974 // draw start of warning range 975 value = this.subrangeInfo[WARNING][RANGE_LOW]; 976 if (range.contains(value)) { 977 double x = midX + COLUMN_RADIUS + 2; 978 double y = this.rangeAxis.valueToJava2D(value, dataArea, 979 RectangleEdge.LEFT); 980 Line2D line = new Line2D.Double(x, y, x + 10, y); 981 g2.setPaint(this.subrangePaint[WARNING]); 982 g2.draw(line); 983 } 984 985 // draw start of critical range 986 value = this.subrangeInfo[CRITICAL][RANGE_LOW]; 987 if (range.contains(value)) { 988 double x = midX + COLUMN_RADIUS + 2; 989 double y = this.rangeAxis.valueToJava2D(value, dataArea, 990 RectangleEdge.LEFT); 991 Line2D line = new Line2D.Double(x, y, x + 10, y); 992 g2.setPaint(this.subrangePaint[CRITICAL]); 993 g2.draw(line); 994 } 995 } 996 997 // draw the axis... 998 if ((this.rangeAxis != null) && (this.axisLocation != NONE)) { 999 int drawWidth = AXIS_GAP; 1000 if (this.showValueLines) { 1001 drawWidth += COLUMN_DIAMETER; 1002 } 1003 Rectangle2D drawArea; 1004 double cursor = 0; 1005 1006 switch (this.axisLocation) { 1007 case RIGHT: 1008 cursor = midX + COLUMN_RADIUS; 1009 drawArea = new Rectangle2D.Double(cursor, 1010 stemTop, drawWidth, (stemBottom - stemTop + 1)); 1011 this.rangeAxis.draw(g2, cursor, area, drawArea, 1012 RectangleEdge.RIGHT, null); 1013 break; 1014 1015 case LEFT: 1016 default: 1017 //cursor = midX - COLUMN_RADIUS - AXIS_GAP; 1018 cursor = midX - COLUMN_RADIUS; 1019 drawArea = new Rectangle2D.Double(cursor, stemTop, 1020 drawWidth, (stemBottom - stemTop + 1)); 1021 this.rangeAxis.draw(g2, cursor, area, drawArea, 1022 RectangleEdge.LEFT, null); 1023 break; 1024 } 1025 1026 } 1027 1028 // draw text value on screen 1029 g2.setFont(this.valueFont); 1030 g2.setPaint(this.valuePaint); 1031 metrics = g2.getFontMetrics(); 1032 switch (this.valueLocation) { 1033 case RIGHT: 1034 g2.drawString(this.valueFormat.format(current), 1035 midX + COLUMN_RADIUS + GAP_RADIUS, midY); 1036 break; 1037 case LEFT: 1038 String valueString = this.valueFormat.format(current); 1039 int stringWidth = metrics.stringWidth(valueString); 1040 g2.drawString(valueString, midX - COLUMN_RADIUS 1041 - GAP_RADIUS - stringWidth, midY); 1042 break; 1043 case BULB: 1044 temp = this.valueFormat.format(current); 1045 i = metrics.stringWidth(temp) / 2; 1046 g2.drawString(temp, midX - i, 1047 stemBottom + BULB_RADIUS + GAP_RADIUS); 1048 break; 1049 default: 1050 } 1051 /***/ 1052 } 1053 1054 g2.setPaint(this.thermometerPaint); 1055 g2.setFont(this.valueFont); 1056 1057 // draw units indicator 1058 metrics = g2.getFontMetrics(); 1059 int tickX1 = midX - COLUMN_RADIUS - GAP_DIAMETER 1060 - metrics.stringWidth(UNITS[this.units]); 1061 if (tickX1 > area.getMinX()) { 1062 g2.drawString(UNITS[this.units], tickX1, 1063 (int) (area.getMinY() + 20)); 1064 } 1065 1066 // draw thermometer outline 1067 g2.setStroke(this.thermometerStroke); 1068 g2.draw(outerThermometer); 1069 g2.draw(innerThermometer); 1070 1071 drawOutline(g2, area); 1072 } 1073 1074 /** 1075 * A zoom method that does nothing. Plots are required to support the 1076 * zoom operation. In the case of a thermometer chart, it doesn't make 1077 * sense to zoom in or out, so the method is empty. 1078 * 1079 * @param percent the zoom percentage. 1080 */ 1081 public void zoom(double percent) { 1082 // intentionally blank 1083 } 1084 1085 /** 1086 * Returns a short string describing the type of plot. 1087 * 1088 * @return A short string describing the type of plot. 1089 */ 1090 public String getPlotType() { 1091 return localizationResources.getString("Thermometer_Plot"); 1092 } 1093 1094 /** 1095 * Checks to see if a new value means the axis range needs adjusting. 1096 * 1097 * @param event the dataset change event. 1098 */ 1099 public void datasetChanged(DatasetChangeEvent event) { 1100 Number vn = this.dataset.getValue(); 1101 if (vn != null) { 1102 double value = vn.doubleValue(); 1103 if (inSubrange(NORMAL, value)) { 1104 this.subrange = NORMAL; 1105 } 1106 else if (inSubrange(WARNING, value)) { 1107 this.subrange = WARNING; 1108 } 1109 else if (inSubrange(CRITICAL, value)) { 1110 this.subrange = CRITICAL; 1111 } 1112 else { 1113 this.subrange = -1; 1114 } 1115 setAxisRange(); 1116 } 1117 super.datasetChanged(event); 1118 } 1119 1120 /** 1121 * Returns the minimum value in either the domain or the range, whichever 1122 * is displayed against the vertical axis for the particular type of plot 1123 * implementing this interface. 1124 * 1125 * @return The minimum value in either the domain or the range. 1126 */ 1127 public Number getMinimumVerticalDataValue() { 1128 return new Double(this.lowerBound); 1129 } 1130 1131 /** 1132 * Returns the maximum value in either the domain or the range, whichever 1133 * is displayed against the vertical axis for the particular type of plot 1134 * implementing this interface. 1135 * 1136 * @return The maximum value in either the domain or the range 1137 */ 1138 public Number getMaximumVerticalDataValue() { 1139 return new Double(this.upperBound); 1140 } 1141 1142 /** 1143 * Returns the data range. 1144 * 1145 * @param axis the axis. 1146 * 1147 * @return The range of data displayed. 1148 */ 1149 public Range getDataRange(ValueAxis axis) { 1150 return new Range(this.lowerBound, this.upperBound); 1151 } 1152 1153 /** 1154 * Sets the axis range to the current values in the rangeInfo array. 1155 */ 1156 protected void setAxisRange() { 1157 if ((this.subrange >= 0) && (this.followDataInSubranges)) { 1158 this.rangeAxis.setRange( 1159 new Range(this.subrangeInfo[this.subrange][DISPLAY_LOW], 1160 this.subrangeInfo[this.subrange][DISPLAY_HIGH])); 1161 } 1162 else { 1163 this.rangeAxis.setRange(this.lowerBound, this.upperBound); 1164 } 1165 } 1166 1167 /** 1168 * Returns the legend items for the plot. 1169 * 1170 * @return <code>null</code>. 1171 */ 1172 public LegendItemCollection getLegendItems() { 1173 return null; 1174 } 1175 1176 /** 1177 * Returns the orientation of the plot. 1178 * 1179 * @return The orientation (always {@link PlotOrientation#VERTICAL}). 1180 */ 1181 public PlotOrientation getOrientation() { 1182 return PlotOrientation.VERTICAL; 1183 } 1184 1185 /** 1186 * Determine whether a number is valid and finite. 1187 * 1188 * @param d the number to be tested. 1189 * 1190 * @return <code>true</code> if the number is valid and finite, and 1191 * <code>false</code> otherwise. 1192 */ 1193 protected static boolean isValidNumber(double d) { 1194 return (!(Double.isNaN(d) || Double.isInfinite(d))); 1195 } 1196 1197 /** 1198 * Returns true if the value is in the specified range, and false otherwise. 1199 * 1200 * @param subrange the subrange. 1201 * @param value the value to check. 1202 * 1203 * @return A boolean. 1204 */ 1205 private boolean inSubrange(int subrange, double value) { 1206 return (value > this.subrangeInfo[subrange][RANGE_LOW] 1207 && value <= this.subrangeInfo[subrange][RANGE_HIGH]); 1208 } 1209 1210 /** 1211 * Returns the mercury paint corresponding to the current data value. 1212 * 1213 * @return The paint. 1214 */ 1215 private Paint getCurrentPaint() { 1216 1217 Paint result = this.mercuryPaint; 1218 if (this.useSubrangePaint) { 1219 double value = this.dataset.getValue().doubleValue(); 1220 if (inSubrange(NORMAL, value)) { 1221 result = this.subrangePaint[NORMAL]; 1222 } 1223 else if (inSubrange(WARNING, value)) { 1224 result = this.subrangePaint[WARNING]; 1225 } 1226 else if (inSubrange(CRITICAL, value)) { 1227 result = this.subrangePaint[CRITICAL]; 1228 } 1229 } 1230 return result; 1231 } 1232 1233 /** 1234 * Tests this plot for equality with another object. The plot's dataset 1235 * is not considered in the test. 1236 * 1237 * @param obj the object (<code>null</code> permitted). 1238 * 1239 * @return <code>true</code> or <code>false</code>. 1240 */ 1241 public boolean equals(Object obj) { 1242 if (obj == this) { 1243 return true; 1244 } 1245 if (!(obj instanceof ThermometerPlot)) { 1246 return false; 1247 } 1248 ThermometerPlot that = (ThermometerPlot) obj; 1249 if (!super.equals(obj)) { 1250 return false; 1251 } 1252 if (!ObjectUtilities.equal(this.rangeAxis, that.rangeAxis)) { 1253 return false; 1254 } 1255 if (this.axisLocation != that.axisLocation) { 1256 return false; 1257 } 1258 if (this.lowerBound != that.lowerBound) { 1259 return false; 1260 } 1261 if (this.upperBound != that.upperBound) { 1262 return false; 1263 } 1264 if (!ObjectUtilities.equal(this.padding, that.padding)) { 1265 return false; 1266 } 1267 if (!ObjectUtilities.equal(this.thermometerStroke, 1268 that.thermometerStroke)) { 1269 return false; 1270 } 1271 if (!PaintUtilities.equal(this.thermometerPaint, 1272 that.thermometerPaint)) { 1273 return false; 1274 } 1275 if (this.units != that.units) { 1276 return false; 1277 } 1278 if (this.valueLocation != that.valueLocation) { 1279 return false; 1280 } 1281 if (!ObjectUtilities.equal(this.valueFont, that.valueFont)) { 1282 return false; 1283 } 1284 if (!PaintUtilities.equal(this.valuePaint, that.valuePaint)) { 1285 return false; 1286 } 1287 if (!ObjectUtilities.equal(this.valueFormat, that.valueFormat)) { 1288 return false; 1289 } 1290 if (!PaintUtilities.equal(this.mercuryPaint, that.mercuryPaint)) { 1291 return false; 1292 } 1293 if (this.showValueLines != that.showValueLines) { 1294 return false; 1295 } 1296 if (this.subrange != that.subrange) { 1297 return false; 1298 } 1299 if (this.followDataInSubranges != that.followDataInSubranges) { 1300 return false; 1301 } 1302 if (!equal(this.subrangeInfo, that.subrangeInfo)) { 1303 return false; 1304 } 1305 if (this.useSubrangePaint != that.useSubrangePaint) { 1306 return false; 1307 } 1308 for (int i = 0; i < this.subrangePaint.length; i++) { 1309 if (!PaintUtilities.equal(this.subrangePaint[i], 1310 that.subrangePaint[i])) { 1311 return false; 1312 } 1313 } 1314 return true; 1315 } 1316 1317 /** 1318 * Tests two double[][] arrays for equality. 1319 * 1320 * @param array1 the first array (<code>null</code> permitted). 1321 * @param array2 the second arrray (<code>null</code> permitted). 1322 * 1323 * @return A boolean. 1324 */ 1325 private static boolean equal(double[][] array1, double[][] array2) { 1326 if (array1 == null) { 1327 return (array2 == null); 1328 } 1329 if (array2 == null) { 1330 return false; 1331 } 1332 if (array1.length != array2.length) { 1333 return false; 1334 } 1335 for (int i = 0; i < array1.length; i++) { 1336 if (!Arrays.equals(array1[i], array2[i])) { 1337 return false; 1338 } 1339 } 1340 return true; 1341 } 1342 1343 /** 1344 * Returns a clone of the plot. 1345 * 1346 * @return A clone. 1347 * 1348 * @throws CloneNotSupportedException if the plot cannot be cloned. 1349 */ 1350 public Object clone() throws CloneNotSupportedException { 1351 1352 ThermometerPlot clone = (ThermometerPlot) super.clone(); 1353 1354 if (clone.dataset != null) { 1355 clone.dataset.addChangeListener(clone); 1356 } 1357 clone.rangeAxis = (ValueAxis) ObjectUtilities.clone(this.rangeAxis); 1358 if (clone.rangeAxis != null) { 1359 clone.rangeAxis.setPlot(clone); 1360 clone.rangeAxis.addChangeListener(clone); 1361 } 1362 clone.valueFormat = (NumberFormat) this.valueFormat.clone(); 1363 clone.subrangePaint = (Paint[]) this.subrangePaint.clone(); 1364 1365 return clone; 1366 1367 } 1368 1369 /** 1370 * Provides serialization support. 1371 * 1372 * @param stream the output stream. 1373 * 1374 * @throws IOException if there is an I/O error. 1375 */ 1376 private void writeObject(ObjectOutputStream stream) throws IOException { 1377 stream.defaultWriteObject(); 1378 SerialUtilities.writeStroke(this.thermometerStroke, stream); 1379 SerialUtilities.writePaint(this.thermometerPaint, stream); 1380 SerialUtilities.writePaint(this.valuePaint, stream); 1381 SerialUtilities.writePaint(this.mercuryPaint, stream); 1382 SerialUtilities.writeStroke(this.subrangeIndicatorStroke, stream); 1383 SerialUtilities.writeStroke(this.rangeIndicatorStroke, stream); 1384 } 1385 1386 /** 1387 * Provides serialization support. 1388 * 1389 * @param stream the input stream. 1390 * 1391 * @throws IOException if there is an I/O error. 1392 * @throws ClassNotFoundException if there is a classpath problem. 1393 */ 1394 private void readObject(ObjectInputStream stream) throws IOException, 1395 ClassNotFoundException { 1396 stream.defaultReadObject(); 1397 this.thermometerStroke = SerialUtilities.readStroke(stream); 1398 this.thermometerPaint = SerialUtilities.readPaint(stream); 1399 this.valuePaint = SerialUtilities.readPaint(stream); 1400 this.mercuryPaint = SerialUtilities.readPaint(stream); 1401 this.subrangeIndicatorStroke = SerialUtilities.readStroke(stream); 1402 this.rangeIndicatorStroke = SerialUtilities.readStroke(stream); 1403 1404 if (this.rangeAxis != null) { 1405 this.rangeAxis.addChangeListener(this); 1406 } 1407 } 1408 1409 /** 1410 * Multiplies the range on the domain axis/axes by the specified factor. 1411 * 1412 * @param factor the zoom factor. 1413 * @param state the plot state. 1414 * @param source the source point. 1415 */ 1416 public void zoomDomainAxes(double factor, PlotRenderingInfo state, 1417 Point2D source) { 1418 // TODO: to be implemented. 1419 } 1420 1421 /** 1422 * Multiplies the range on the range axis/axes by the specified factor. 1423 * 1424 * @param factor the zoom factor. 1425 * @param state the plot state. 1426 * @param source the source point. 1427 */ 1428 public void zoomRangeAxes(double factor, PlotRenderingInfo state, 1429 Point2D source) { 1430 this.rangeAxis.resizeRange(factor); 1431 } 1432 1433 /** 1434 * This method does nothing. 1435 * 1436 * @param lowerPercent the lower percent. 1437 * @param upperPercent the upper percent. 1438 * @param state the plot state. 1439 * @param source the source point. 1440 */ 1441 public void zoomDomainAxes(double lowerPercent, double upperPercent, 1442 PlotRenderingInfo state, Point2D source) { 1443 // no domain axis to zoom 1444 } 1445 1446 /** 1447 * Zooms the range axes. 1448 * 1449 * @param lowerPercent the lower percent. 1450 * @param upperPercent the upper percent. 1451 * @param state the plot state. 1452 * @param source the source point. 1453 */ 1454 public void zoomRangeAxes(double lowerPercent, double upperPercent, 1455 PlotRenderingInfo state, Point2D source) { 1456 this.rangeAxis.zoomRange(lowerPercent, upperPercent); 1457 } 1458 1459 /** 1460 * Returns <code>false</code>. 1461 * 1462 * @return A boolean. 1463 */ 1464 public boolean isDomainZoomable() { 1465 return false; 1466 } 1467 1468 /** 1469 * Returns <code>true</code>. 1470 * 1471 * @return A boolean. 1472 */ 1473 public boolean isRangeZoomable() { 1474 return true; 1475 } 1476 1477 }