001 /* =========================================================== 002 * JFreeChart : a free chart library for the Java(tm) platform 003 * =========================================================== 004 * 005 * (C) Copyright 2000-2005, 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-2005, 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 * 076 */ 077 078 package org.jfree.chart.plot; 079 080 import java.awt.BasicStroke; 081 import java.awt.Color; 082 import java.awt.Font; 083 import java.awt.FontMetrics; 084 import java.awt.Graphics2D; 085 import java.awt.Paint; 086 import java.awt.Stroke; 087 import java.awt.geom.Area; 088 import java.awt.geom.Ellipse2D; 089 import java.awt.geom.Line2D; 090 import java.awt.geom.Point2D; 091 import java.awt.geom.Rectangle2D; 092 import java.awt.geom.RoundRectangle2D; 093 import java.io.IOException; 094 import java.io.ObjectInputStream; 095 import java.io.ObjectOutputStream; 096 import java.io.Serializable; 097 import java.text.DecimalFormat; 098 import java.text.NumberFormat; 099 import java.util.Arrays; 100 import java.util.ResourceBundle; 101 102 import org.jfree.chart.LegendItemCollection; 103 import org.jfree.chart.axis.NumberAxis; 104 import org.jfree.chart.axis.ValueAxis; 105 import org.jfree.chart.event.PlotChangeEvent; 106 import org.jfree.data.Range; 107 import org.jfree.data.general.DatasetChangeEvent; 108 import org.jfree.data.general.DefaultValueDataset; 109 import org.jfree.data.general.ValueDataset; 110 import org.jfree.io.SerialUtilities; 111 import org.jfree.ui.RectangleEdge; 112 import org.jfree.ui.RectangleInsets; 113 import org.jfree.util.ObjectUtilities; 114 import org.jfree.util.PaintUtilities; 115 import org.jfree.util.UnitType; 116 117 /** 118 * A plot that displays a single value (from a {@link ValueDataset}) in a 119 * thermometer type display. 120 * <p> 121 * This plot supports a number of options: 122 * <ol> 123 * <li>three sub-ranges which could be viewed as 'Normal', 'Warning' 124 * and 'Critical' ranges.</li> 125 * <li>the thermometer can be run in two modes: 126 * <ul> 127 * <li>fixed range, or</li> 128 * <li>range adjusts to current sub-range.</li> 129 * </ul> 130 * </li> 131 * <li>settable units to be displayed.</li> 132 * <li>settable display location for the value text.</li> 133 * </ol> 134 * 135 * @author Bryan Scott 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( 327 UnitType.RELATIVE, 0.05, 0.05, 0.05, 0.05 328 ); 329 this.dataset = dataset; 330 if (dataset != null) { 331 dataset.addChangeListener(this); 332 } 333 NumberAxis axis = new NumberAxis(null); 334 axis.setStandardTickUnits(NumberAxis.createIntegerTickUnits()); 335 axis.setAxisLineVisible(false); 336 337 setRangeAxis(axis); 338 setAxisRange(); 339 } 340 341 /** 342 * Returns the primary dataset for the plot. 343 * 344 * @return The primary dataset (possibly <code>null</code>). 345 */ 346 public ValueDataset getDataset() { 347 return this.dataset; 348 } 349 350 /** 351 * Sets the dataset for the plot, replacing the existing dataset if there 352 * is one. 353 * 354 * @param dataset the dataset (<code>null</code> permitted). 355 */ 356 public void setDataset(ValueDataset dataset) { 357 358 // if there is an existing dataset, remove the plot from the list 359 // of change listeners... 360 ValueDataset existing = this.dataset; 361 if (existing != null) { 362 existing.removeChangeListener(this); 363 } 364 365 // set the new dataset, and register the chart as a change listener... 366 this.dataset = dataset; 367 if (dataset != null) { 368 setDatasetGroup(dataset.getGroup()); 369 dataset.addChangeListener(this); 370 } 371 372 // send a dataset change event to self... 373 DatasetChangeEvent event = new DatasetChangeEvent(this, dataset); 374 datasetChanged(event); 375 376 } 377 378 /** 379 * Returns the range axis. 380 * 381 * @return The range axis. 382 */ 383 public ValueAxis getRangeAxis() { 384 return this.rangeAxis; 385 } 386 387 /** 388 * Sets the range axis for the plot. 389 * 390 * @param axis the new axis. 391 */ 392 public void setRangeAxis(ValueAxis axis) { 393 394 if (axis != null) { 395 axis.setPlot(this); 396 axis.addChangeListener(this); 397 } 398 399 // plot is likely registered as a listener with the existing axis... 400 if (this.rangeAxis != null) { 401 this.rangeAxis.removeChangeListener(this); 402 } 403 404 this.rangeAxis = axis; 405 406 } 407 408 /** 409 * Returns the lower bound for the thermometer. The data value can be set 410 * lower than this, but it will not be shown in the thermometer. 411 * 412 * @return The lower bound. 413 * 414 */ 415 public double getLowerBound() { 416 return this.lowerBound; 417 } 418 419 /** 420 * Sets the lower bound for the thermometer. 421 * 422 * @param lower the lower bound. 423 */ 424 public void setLowerBound(double lower) { 425 this.lowerBound = lower; 426 setAxisRange(); 427 } 428 429 /** 430 * Returns the upper bound for the thermometer. The data value can be set 431 * higher than this, but it will not be shown in the thermometer. 432 * 433 * @return The upper bound. 434 */ 435 public double getUpperBound() { 436 return this.upperBound; 437 } 438 439 /** 440 * Sets the upper bound for the thermometer. 441 * 442 * @param upper the upper bound. 443 */ 444 public void setUpperBound(double upper) { 445 this.upperBound = upper; 446 setAxisRange(); 447 } 448 449 /** 450 * Sets the lower and upper bounds for the thermometer. 451 * 452 * @param lower the lower bound. 453 * @param upper the upper bound. 454 */ 455 public void setRange(double lower, double upper) { 456 this.lowerBound = lower; 457 this.upperBound = upper; 458 setAxisRange(); 459 } 460 461 /** 462 * Returns the padding for the thermometer. This is the space inside the 463 * plot area. 464 * 465 * @return The padding. 466 */ 467 public RectangleInsets getPadding() { 468 return this.padding; 469 } 470 471 /** 472 * Sets the padding for the thermometer. 473 * 474 * @param padding the padding. 475 */ 476 public void setPadding(RectangleInsets padding) { 477 this.padding = padding; 478 notifyListeners(new PlotChangeEvent(this)); 479 } 480 481 /** 482 * Returns the stroke used to draw the thermometer outline. 483 * 484 * @return The stroke. 485 */ 486 public Stroke getThermometerStroke() { 487 return this.thermometerStroke; 488 } 489 490 /** 491 * Sets the stroke used to draw the thermometer outline. 492 * 493 * @param s the new stroke (null ignored). 494 */ 495 public void setThermometerStroke(Stroke s) { 496 if (s != null) { 497 this.thermometerStroke = s; 498 notifyListeners(new PlotChangeEvent(this)); 499 } 500 } 501 502 /** 503 * Returns the paint used to draw the thermometer outline. 504 * 505 * @return The paint. 506 */ 507 public Paint getThermometerPaint() { 508 return this.thermometerPaint; 509 } 510 511 /** 512 * Sets the paint used to draw the thermometer outline. 513 * 514 * @param paint the new paint (null ignored). 515 */ 516 public void setThermometerPaint(Paint paint) { 517 if (paint != null) { 518 this.thermometerPaint = paint; 519 notifyListeners(new PlotChangeEvent(this)); 520 } 521 } 522 523 /** 524 * Returns the unit display type (none/Fahrenheit/Celcius/Kelvin). 525 * 526 * @return The units type. 527 */ 528 public int getUnits() { 529 return this.units; 530 } 531 532 /** 533 * Sets the units to be displayed in the thermometer. 534 * <p> 535 * Use one of the following constants: 536 * 537 * <ul> 538 * <li>UNITS_NONE : no units displayed.</li> 539 * <li>UNITS_FAHRENHEIT : units displayed in Fahrenheit.</li> 540 * <li>UNITS_CELCIUS : units displayed in Celcius.</li> 541 * <li>UNITS_KELVIN : units displayed in Kelvin.</li> 542 * </ul> 543 * 544 * @param u the new unit type. 545 */ 546 public void setUnits(int u) { 547 if ((u >= 0) && (u < UNITS.length)) { 548 if (this.units != u) { 549 this.units = u; 550 notifyListeners(new PlotChangeEvent(this)); 551 } 552 } 553 } 554 555 /** 556 * Sets the unit type. 557 * 558 * @param u the unit type (null ignored). 559 */ 560 public void setUnits(String u) { 561 if (u == null) { 562 return; 563 } 564 565 u = u.toUpperCase().trim(); 566 for (int i = 0; i < UNITS.length; ++i) { 567 if (u.equals(UNITS[i].toUpperCase().trim())) { 568 setUnits(i); 569 i = UNITS.length; 570 } 571 } 572 } 573 574 /** 575 * Returns the value location. 576 * 577 * @return The location. 578 */ 579 public int getValueLocation() { 580 return this.valueLocation; 581 } 582 583 /** 584 * Sets the location at which the current value is displayed. 585 * <P> 586 * The location can be one of the constants: 587 * <code>NONE</code>, 588 * <code>RIGHT</code> 589 * <code>LEFT</code> and 590 * <code>BULB</code>. 591 * 592 * @param location the location. 593 */ 594 public void setValueLocation(int location) { 595 if ((location >= 0) && (location < 4)) { 596 this.valueLocation = location; 597 notifyListeners(new PlotChangeEvent(this)); 598 } 599 else { 600 throw new IllegalArgumentException("Location not recognised."); 601 } 602 } 603 604 /** 605 * Sets the location at which the axis is displayed with reference to the 606 * bulb. 607 * <P> 608 * The location can be one of the constants: 609 * <code>NONE</code>, 610 * <code>RIGHT</code> and 611 * <code>LEFT</code>. 612 * 613 * @param location the location. 614 */ 615 public void setAxisLocation(int location) { 616 if ((location >= 0) && (location < 3)) { 617 this.axisLocation = location; 618 notifyListeners(new PlotChangeEvent(this)); 619 } 620 else { 621 throw new IllegalArgumentException("Location not recognised."); 622 } 623 } 624 625 /** 626 * Returns the axis location. 627 * 628 * @return The location. 629 */ 630 public int getAxisLocation() { 631 return this.axisLocation; 632 } 633 634 /** 635 * Gets the font used to display the current value. 636 * 637 * @return The font. 638 */ 639 public Font getValueFont() { 640 return this.valueFont; 641 } 642 643 /** 644 * Sets the font used to display the current value. 645 * 646 * @param f the new font. 647 */ 648 public void setValueFont(Font f) { 649 if ((f != null) && (!this.valueFont.equals(f))) { 650 this.valueFont = f; 651 notifyListeners(new PlotChangeEvent(this)); 652 } 653 } 654 655 /** 656 * Gets the paint used to display the current value. 657 * 658 * @return The paint. 659 */ 660 public Paint getValuePaint() { 661 return this.valuePaint; 662 } 663 664 /** 665 * Sets the paint used to display the current value. 666 * 667 * @param p the new paint. 668 */ 669 public void setValuePaint(Paint p) { 670 if ((p != null) && (!this.valuePaint.equals(p))) { 671 this.valuePaint = p; 672 notifyListeners(new PlotChangeEvent(this)); 673 } 674 } 675 676 /** 677 * Sets the formatter for the value label. 678 * 679 * @param formatter the new formatter. 680 */ 681 public void setValueFormat(NumberFormat formatter) { 682 if (formatter != null) { 683 this.valueFormat = formatter; 684 notifyListeners(new PlotChangeEvent(this)); 685 } 686 } 687 688 /** 689 * Returns the default mercury paint. 690 * 691 * @return The paint. 692 */ 693 public Paint getMercuryPaint() { 694 return this.mercuryPaint; 695 } 696 697 /** 698 * Sets the default mercury paint. 699 * 700 * @param paint the new paint. 701 */ 702 public void setMercuryPaint(Paint paint) { 703 this.mercuryPaint = paint; 704 notifyListeners(new PlotChangeEvent(this)); 705 } 706 707 /** 708 * Returns the flag that controls whether not value lines are displayed. 709 * 710 * @return The flag. 711 */ 712 public boolean getShowValueLines() { 713 return this.showValueLines; 714 } 715 716 /** 717 * Sets the display as to whether to show value lines in the output. 718 * 719 * @param b Whether to show value lines in the thermometer 720 */ 721 public void setShowValueLines(boolean b) { 722 this.showValueLines = b; 723 notifyListeners(new PlotChangeEvent(this)); 724 } 725 726 /** 727 * Sets information for a particular range. 728 * 729 * @param range the range to specify information about. 730 * @param low the low value for the range 731 * @param hi the high value for the range 732 */ 733 public void setSubrangeInfo(int range, double low, double hi) { 734 setSubrangeInfo(range, low, hi, low, hi); 735 } 736 737 /** 738 * Sets the subrangeInfo attribute of the ThermometerPlot object 739 * 740 * @param range the new rangeInfo value. 741 * @param rangeLow the new rangeInfo value 742 * @param rangeHigh the new rangeInfo value 743 * @param displayLow the new rangeInfo value 744 * @param displayHigh the new rangeInfo value 745 */ 746 public void setSubrangeInfo(int range, 747 double rangeLow, double rangeHigh, 748 double displayLow, double displayHigh) { 749 750 if ((range >= 0) && (range < 3)) { 751 setSubrange(range, rangeLow, rangeHigh); 752 setDisplayRange(range, displayLow, displayHigh); 753 setAxisRange(); 754 notifyListeners(new PlotChangeEvent(this)); 755 } 756 757 } 758 759 /** 760 * Sets the range. 761 * 762 * @param range the range type. 763 * @param low the low value. 764 * @param high the high value. 765 */ 766 public void setSubrange(int range, double low, double high) { 767 if ((range >= 0) && (range < 3)) { 768 this.subrangeInfo[range][RANGE_HIGH] = high; 769 this.subrangeInfo[range][RANGE_LOW] = low; 770 } 771 } 772 773 /** 774 * Sets the display range. 775 * 776 * @param range the range type. 777 * @param low the low value. 778 * @param high the high value. 779 */ 780 public void setDisplayRange(int range, double low, double high) { 781 782 if ((range >= 0) && (range < this.subrangeInfo.length) 783 && isValidNumber(high) && isValidNumber(low)) { 784 785 if (high > low) { 786 this.subrangeInfo[range][DISPLAY_HIGH] = high; 787 this.subrangeInfo[range][DISPLAY_LOW] = low; 788 } 789 else { 790 this.subrangeInfo[range][DISPLAY_HIGH] = low; 791 this.subrangeInfo[range][DISPLAY_LOW] = high; 792 } 793 794 } 795 796 } 797 798 /** 799 * Gets the paint used for a particular subrange. 800 * 801 * @param range the range. 802 * 803 * @return The paint. 804 */ 805 public Paint getSubrangePaint(int range) { 806 if ((range >= 0) && (range < this.subrangePaint.length)) { 807 return this.subrangePaint[range]; 808 } 809 else { 810 return this.mercuryPaint; 811 } 812 } 813 814 /** 815 * Sets the paint to be used for a range. 816 * 817 * @param range the range. 818 * @param paint the paint to be applied. 819 */ 820 public void setSubrangePaint(int range, Paint paint) { 821 if ((range >= 0) 822 && (range < this.subrangePaint.length) && (paint != null)) { 823 this.subrangePaint[range] = paint; 824 notifyListeners(new PlotChangeEvent(this)); 825 } 826 } 827 828 /** 829 * Returns a flag that controls whether or not the thermometer axis zooms 830 * to display the subrange within which the data value falls. 831 * 832 * @return The flag. 833 */ 834 public boolean getFollowDataInSubranges() { 835 return this.followDataInSubranges; 836 } 837 838 /** 839 * Sets the flag that controls whether or not the thermometer axis zooms 840 * to display the subrange within which the data value falls. 841 * 842 * @param flag the flag. 843 */ 844 public void setFollowDataInSubranges(boolean flag) { 845 this.followDataInSubranges = flag; 846 notifyListeners(new PlotChangeEvent(this)); 847 } 848 849 /** 850 * Returns a flag that controls whether or not the mercury color changes 851 * for each subrange. 852 * 853 * @return The flag. 854 */ 855 public boolean getUseSubrangePaint() { 856 return this.useSubrangePaint; 857 } 858 859 /** 860 * Sets the range colour change option. 861 * 862 * @param flag The new range colour change option 863 */ 864 public void setUseSubrangePaint(boolean flag) { 865 this.useSubrangePaint = flag; 866 notifyListeners(new PlotChangeEvent(this)); 867 } 868 869 /** 870 * Draws the plot on a Java 2D graphics device (such as the screen or a 871 * printer). 872 * 873 * @param g2 the graphics device. 874 * @param area the area within which the plot should be drawn. 875 * @param anchor the anchor point (<code>null</code> permitted). 876 * @param parentState the state from the parent plot, if there is one. 877 * @param info collects info about the drawing. 878 */ 879 public void draw(Graphics2D g2, Rectangle2D area, Point2D anchor, 880 PlotState parentState, 881 PlotRenderingInfo info) { 882 883 RoundRectangle2D outerStem = new RoundRectangle2D.Double(); 884 RoundRectangle2D innerStem = new RoundRectangle2D.Double(); 885 RoundRectangle2D mercuryStem = new RoundRectangle2D.Double(); 886 Ellipse2D outerBulb = new Ellipse2D.Double(); 887 Ellipse2D innerBulb = new Ellipse2D.Double(); 888 String temp = null; 889 FontMetrics metrics = null; 890 if (info != null) { 891 info.setPlotArea(area); 892 } 893 894 // adjust for insets... 895 RectangleInsets insets = getInsets(); 896 insets.trim(area); 897 drawBackground(g2, area); 898 899 // adjust for padding... 900 //this.padding.trim(plotArea); 901 int midX = (int) (area.getX() + (area.getWidth() / 2)); 902 int midY = (int) (area.getY() + (area.getHeight() / 2)); 903 int stemTop = (int) (area.getMinY() + BULB_RADIUS); 904 int stemBottom = (int) (area.getMaxY() - BULB_DIAMETER); 905 Rectangle2D dataArea = new Rectangle2D.Double( 906 midX - COLUMN_RADIUS, stemTop, COLUMN_RADIUS, stemBottom - stemTop 907 ); 908 909 outerBulb.setFrame( 910 midX - BULB_RADIUS, stemBottom, BULB_DIAMETER, BULB_DIAMETER 911 ); 912 913 outerStem.setRoundRect( 914 midX - COLUMN_RADIUS, area.getMinY(), COLUMN_DIAMETER, 915 stemBottom + BULB_DIAMETER - stemTop, 916 COLUMN_DIAMETER, COLUMN_DIAMETER 917 ); 918 919 Area outerThermometer = new Area(outerBulb); 920 Area tempArea = new Area(outerStem); 921 outerThermometer.add(tempArea); 922 923 innerBulb.setFrame( 924 midX - BULB_RADIUS + GAP_RADIUS, stemBottom + GAP_RADIUS, 925 BULB_DIAMETER - GAP_DIAMETER, BULB_DIAMETER - GAP_DIAMETER 926 ); 927 928 innerStem.setRoundRect( 929 midX - COLUMN_RADIUS + GAP_RADIUS, area.getMinY() + GAP_RADIUS, 930 COLUMN_DIAMETER - GAP_DIAMETER, 931 stemBottom + BULB_DIAMETER - GAP_DIAMETER - stemTop, 932 COLUMN_DIAMETER - GAP_DIAMETER, COLUMN_DIAMETER - GAP_DIAMETER 933 ); 934 935 Area innerThermometer = new Area(innerBulb); 936 tempArea = new Area(innerStem); 937 innerThermometer.add(tempArea); 938 939 if ((this.dataset != null) && (this.dataset.getValue() != null)) { 940 double current = this.dataset.getValue().doubleValue(); 941 double ds = this.rangeAxis.valueToJava2D( 942 current, dataArea, RectangleEdge.LEFT 943 ); 944 945 int i = COLUMN_DIAMETER - GAP_DIAMETER; // already calculated 946 int j = COLUMN_RADIUS - GAP_RADIUS; // already calculated 947 int l = (i / 2); 948 int k = (int) Math.round(ds); 949 if (k < (GAP_RADIUS + area.getMinY())) { 950 k = (int) (GAP_RADIUS + area.getMinY()); 951 l = BULB_RADIUS; 952 } 953 954 Area mercury = new Area(innerBulb); 955 956 if (k < (stemBottom + BULB_RADIUS)) { 957 mercuryStem.setRoundRect( 958 midX - j, k, i, (stemBottom + BULB_RADIUS) - k, l, l 959 ); 960 tempArea = new Area(mercuryStem); 961 mercury.add(tempArea); 962 } 963 964 g2.setPaint(getCurrentPaint()); 965 g2.fill(mercury); 966 967 // draw range indicators... 968 if (this.subrangeIndicatorsVisible) { 969 g2.setStroke(this.subrangeIndicatorStroke); 970 Range range = this.rangeAxis.getRange(); 971 972 // draw start of normal range 973 double value = this.subrangeInfo[NORMAL][RANGE_LOW]; 974 if (range.contains(value)) { 975 double x = midX + COLUMN_RADIUS + 2; 976 double y = this.rangeAxis.valueToJava2D( 977 value, dataArea, RectangleEdge.LEFT 978 ); 979 Line2D line = new Line2D.Double(x, y, x + 10, y); 980 g2.setPaint(this.subrangePaint[NORMAL]); 981 g2.draw(line); 982 } 983 984 // draw start of warning range 985 value = this.subrangeInfo[WARNING][RANGE_LOW]; 986 if (range.contains(value)) { 987 double x = midX + COLUMN_RADIUS + 2; 988 double y = this.rangeAxis.valueToJava2D( 989 value, dataArea, RectangleEdge.LEFT 990 ); 991 Line2D line = new Line2D.Double(x, y, x + 10, y); 992 g2.setPaint(this.subrangePaint[WARNING]); 993 g2.draw(line); 994 } 995 996 // draw start of critical range 997 value = this.subrangeInfo[CRITICAL][RANGE_LOW]; 998 if (range.contains(value)) { 999 double x = midX + COLUMN_RADIUS + 2; 1000 double y = this.rangeAxis.valueToJava2D( 1001 value, dataArea, RectangleEdge.LEFT 1002 ); 1003 Line2D line = new Line2D.Double(x, y, x + 10, y); 1004 g2.setPaint(this.subrangePaint[CRITICAL]); 1005 g2.draw(line); 1006 } 1007 } 1008 1009 // draw the axis... 1010 if ((this.rangeAxis != null) && (this.axisLocation != NONE)) { 1011 int drawWidth = AXIS_GAP; 1012 if (this.showValueLines) { 1013 drawWidth += COLUMN_DIAMETER; 1014 } 1015 Rectangle2D drawArea; 1016 double cursor = 0; 1017 1018 switch (this.axisLocation) { 1019 case RIGHT: 1020 cursor = midX + COLUMN_RADIUS; 1021 drawArea = new Rectangle2D.Double( 1022 cursor, 1023 stemTop, 1024 drawWidth, 1025 (stemBottom - stemTop + 1) 1026 ); 1027 this.rangeAxis.draw( 1028 g2, cursor, area, drawArea, 1029 RectangleEdge.RIGHT, null 1030 ); 1031 break; 1032 1033 case LEFT: 1034 default: 1035 //cursor = midX - COLUMN_RADIUS - AXIS_GAP; 1036 cursor = midX - COLUMN_RADIUS; 1037 drawArea = new Rectangle2D.Double( 1038 cursor, 1039 stemTop, 1040 drawWidth, 1041 (stemBottom - stemTop + 1) 1042 ); 1043 this.rangeAxis.draw( 1044 g2, cursor, area, drawArea, 1045 RectangleEdge.LEFT, null 1046 ); 1047 break; 1048 } 1049 1050 } 1051 1052 // draw text value on screen 1053 g2.setFont(this.valueFont); 1054 g2.setPaint(this.valuePaint); 1055 metrics = g2.getFontMetrics(); 1056 switch (this.valueLocation) { 1057 case RIGHT: 1058 g2.drawString( 1059 this.valueFormat.format(current), 1060 midX + COLUMN_RADIUS + GAP_RADIUS, midY 1061 ); 1062 break; 1063 case LEFT: 1064 String valueString = this.valueFormat.format(current); 1065 int stringWidth = metrics.stringWidth(valueString); 1066 g2.drawString( 1067 valueString, 1068 midX - COLUMN_RADIUS - GAP_RADIUS - stringWidth, midY 1069 ); 1070 break; 1071 case BULB: 1072 temp = this.valueFormat.format(current); 1073 i = metrics.stringWidth(temp) / 2; 1074 g2.drawString( 1075 temp, midX - i, 1076 stemBottom + BULB_RADIUS + GAP_RADIUS 1077 ); 1078 break; 1079 default: 1080 } 1081 /***/ 1082 } 1083 1084 g2.setPaint(this.thermometerPaint); 1085 g2.setFont(this.valueFont); 1086 1087 // draw units indicator 1088 metrics = g2.getFontMetrics(); 1089 int tickX1 = midX - COLUMN_RADIUS - GAP_DIAMETER 1090 - metrics.stringWidth(UNITS[this.units]); 1091 if (tickX1 > area.getMinX()) { 1092 g2.drawString( 1093 UNITS[this.units], tickX1, (int) (area.getMinY() + 20) 1094 ); 1095 } 1096 1097 // draw thermometer outline 1098 g2.setStroke(this.thermometerStroke); 1099 g2.draw(outerThermometer); 1100 g2.draw(innerThermometer); 1101 1102 drawOutline(g2, area); 1103 } 1104 1105 /** 1106 * A zoom method that does nothing. Plots are required to support the 1107 * zoom operation. In the case of a thermometer chart, it doesn't make 1108 * sense to zoom in or out, so the method is empty. 1109 * 1110 * @param percent the zoom percentage. 1111 */ 1112 public void zoom(double percent) { 1113 // intentionally blank 1114 } 1115 1116 /** 1117 * Returns a short string describing the type of plot. 1118 * 1119 * @return A short string describing the type of plot. 1120 */ 1121 public String getPlotType() { 1122 return localizationResources.getString("Thermometer_Plot"); 1123 } 1124 1125 /** 1126 * Checks to see if a new value means the axis range needs adjusting. 1127 * 1128 * @param event the dataset change event. 1129 */ 1130 public void datasetChanged(DatasetChangeEvent event) { 1131 Number vn = this.dataset.getValue(); 1132 if (vn != null) { 1133 double value = vn.doubleValue(); 1134 if (inSubrange(NORMAL, value)) { 1135 this.subrange = NORMAL; 1136 } 1137 else if (inSubrange(WARNING, value)) { 1138 this.subrange = WARNING; 1139 } 1140 else if (inSubrange(CRITICAL, value)) { 1141 this.subrange = CRITICAL; 1142 } 1143 else { 1144 this.subrange = -1; 1145 } 1146 setAxisRange(); 1147 } 1148 super.datasetChanged(event); 1149 } 1150 1151 /** 1152 * Returns the minimum value in either the domain or the range, whichever 1153 * is displayed against the vertical axis for the particular type of plot 1154 * implementing this interface. 1155 * 1156 * @return The minimum value in either the domain or the range. 1157 */ 1158 public Number getMinimumVerticalDataValue() { 1159 return new Double(this.lowerBound); 1160 } 1161 1162 /** 1163 * Returns the maximum value in either the domain or the range, whichever 1164 * is displayed against the vertical axis for the particular type of plot 1165 * implementing this interface. 1166 * 1167 * @return The maximum value in either the domain or the range 1168 */ 1169 public Number getMaximumVerticalDataValue() { 1170 return new Double(this.upperBound); 1171 } 1172 1173 /** 1174 * Returns the data range. 1175 * 1176 * @param axis the axis. 1177 * 1178 * @return The range of data displayed. 1179 */ 1180 public Range getDataRange(ValueAxis axis) { 1181 return new Range(this.lowerBound, this.upperBound); 1182 } 1183 1184 /** 1185 * Sets the axis range to the current values in the rangeInfo array. 1186 */ 1187 protected void setAxisRange() { 1188 if ((this.subrange >= 0) && (this.followDataInSubranges)) { 1189 this.rangeAxis.setRange( 1190 new Range(this.subrangeInfo[this.subrange][DISPLAY_LOW], 1191 this.subrangeInfo[this.subrange][DISPLAY_HIGH]) 1192 ); 1193 } 1194 else { 1195 this.rangeAxis.setRange(this.lowerBound, this.upperBound); 1196 } 1197 } 1198 1199 /** 1200 * Returns the legend items for the plot. 1201 * 1202 * @return <code>null</code>. 1203 */ 1204 public LegendItemCollection getLegendItems() { 1205 return null; 1206 } 1207 1208 /** 1209 * Returns the orientation of the plot. 1210 * 1211 * @return The orientation (always {@link PlotOrientation#VERTICAL}). 1212 */ 1213 public PlotOrientation getOrientation() { 1214 return PlotOrientation.VERTICAL; 1215 } 1216 1217 /** 1218 * Determine whether a number is valid and finite. 1219 * 1220 * @param d the number to be tested. 1221 * 1222 * @return <code>true</code> if the number is valid and finite, and 1223 * <code>false</code> otherwise. 1224 */ 1225 protected static boolean isValidNumber(double d) { 1226 return (!(Double.isNaN(d) || Double.isInfinite(d))); 1227 } 1228 1229 /** 1230 * Returns true if the value is in the specified range, and false otherwise. 1231 * 1232 * @param subrange the subrange. 1233 * @param value the value to check. 1234 * 1235 * @return A boolean. 1236 */ 1237 private boolean inSubrange(int subrange, double value) { 1238 return (value > this.subrangeInfo[subrange][RANGE_LOW] 1239 && value <= this.subrangeInfo[subrange][RANGE_HIGH]); 1240 } 1241 1242 /** 1243 * Returns the mercury paint corresponding to the current data value. 1244 * 1245 * @return The paint. 1246 */ 1247 private Paint getCurrentPaint() { 1248 1249 Paint result = this.mercuryPaint; 1250 if (this.useSubrangePaint) { 1251 double value = this.dataset.getValue().doubleValue(); 1252 if (inSubrange(NORMAL, value)) { 1253 result = this.subrangePaint[NORMAL]; 1254 } 1255 else if (inSubrange(WARNING, value)) { 1256 result = this.subrangePaint[WARNING]; 1257 } 1258 else if (inSubrange(CRITICAL, value)) { 1259 result = this.subrangePaint[CRITICAL]; 1260 } 1261 } 1262 return result; 1263 } 1264 1265 /** 1266 * Tests this plot for equality with another object. The plot's dataset 1267 * is not considered in the test. 1268 * 1269 * @param obj the object (<code>null</code> permitted). 1270 * 1271 * @return <code>true</code> or <code>false</code>. 1272 */ 1273 public boolean equals(Object obj) { 1274 if (obj == this) { 1275 return true; 1276 } 1277 if (!(obj instanceof ThermometerPlot)) { 1278 return false; 1279 } 1280 ThermometerPlot that = (ThermometerPlot) obj; 1281 if (!super.equals(obj)) { 1282 return false; 1283 } 1284 if (!ObjectUtilities.equal(this.rangeAxis, that.rangeAxis)) { 1285 return false; 1286 } 1287 if (this.axisLocation != that.axisLocation) { 1288 return false; 1289 } 1290 if (this.lowerBound != that.lowerBound) { 1291 return false; 1292 } 1293 if (this.upperBound != that.upperBound) { 1294 return false; 1295 } 1296 if (!ObjectUtilities.equal(this.padding, that.padding)) { 1297 return false; 1298 } 1299 if (!ObjectUtilities.equal( 1300 this.thermometerStroke, that.thermometerStroke 1301 )) { 1302 return false; 1303 } 1304 if (!PaintUtilities.equal( 1305 this.thermometerPaint, that.thermometerPaint 1306 )) { 1307 return false; 1308 } 1309 if (this.units != that.units) { 1310 return false; 1311 } 1312 if (this.valueLocation != that.valueLocation) { 1313 return false; 1314 } 1315 if (!ObjectUtilities.equal(this.valueFont, that.valueFont)) { 1316 return false; 1317 } 1318 if (!PaintUtilities.equal(this.valuePaint, that.valuePaint)) { 1319 return false; 1320 } 1321 if (!ObjectUtilities.equal(this.valueFormat, that.valueFormat)) { 1322 return false; 1323 } 1324 if (!PaintUtilities.equal(this.mercuryPaint, that.mercuryPaint)) { 1325 return false; 1326 } 1327 if (this.showValueLines != that.showValueLines) { 1328 return false; 1329 } 1330 if (this.subrange != that.subrange) { 1331 return false; 1332 } 1333 if (this.followDataInSubranges != that.followDataInSubranges) { 1334 return false; 1335 } 1336 if (!equal(this.subrangeInfo, that.subrangeInfo)) { 1337 return false; 1338 } 1339 if (this.useSubrangePaint != that.useSubrangePaint) { 1340 return false; 1341 } 1342 for (int i = 0; i < this.subrangePaint.length; i++) { 1343 if (!PaintUtilities.equal(this.subrangePaint[i], 1344 that.subrangePaint[i])) { 1345 return false; 1346 } 1347 } 1348 return true; 1349 } 1350 1351 /** 1352 * Tests two double[][] arrays for equality. 1353 * 1354 * @param array1 the first array (<code>null</code> permitted). 1355 * @param array2 the second arrray (<code>null</code> permitted). 1356 * 1357 * @return A boolean. 1358 */ 1359 private static boolean equal(double[][] array1, double[][] array2) { 1360 if (array1 == null) { 1361 return (array2 == null); 1362 } 1363 if (array2 == null) { 1364 return false; 1365 } 1366 if (array1.length != array2.length) { 1367 return false; 1368 } 1369 for (int i = 0; i < array1.length; i++) { 1370 if (!Arrays.equals(array1[i], array2[i])) { 1371 return false; 1372 } 1373 } 1374 return true; 1375 } 1376 1377 /** 1378 * Returns a clone of the plot. 1379 * 1380 * @return A clone. 1381 * 1382 * @throws CloneNotSupportedException if the plot cannot be cloned. 1383 */ 1384 public Object clone() throws CloneNotSupportedException { 1385 1386 ThermometerPlot clone = (ThermometerPlot) super.clone(); 1387 1388 if (clone.dataset != null) { 1389 clone.dataset.addChangeListener(clone); 1390 } 1391 clone.rangeAxis = (ValueAxis) ObjectUtilities.clone(this.rangeAxis); 1392 if (clone.rangeAxis != null) { 1393 clone.rangeAxis.setPlot(clone); 1394 clone.rangeAxis.addChangeListener(clone); 1395 } 1396 clone.valueFormat = (NumberFormat) this.valueFormat.clone(); 1397 clone.subrangePaint = (Paint[]) this.subrangePaint.clone(); 1398 1399 return clone; 1400 1401 } 1402 1403 /** 1404 * Provides serialization support. 1405 * 1406 * @param stream the output stream. 1407 * 1408 * @throws IOException if there is an I/O error. 1409 */ 1410 private void writeObject(ObjectOutputStream stream) throws IOException { 1411 stream.defaultWriteObject(); 1412 SerialUtilities.writeStroke(this.thermometerStroke, stream); 1413 SerialUtilities.writePaint(this.thermometerPaint, stream); 1414 SerialUtilities.writePaint(this.valuePaint, stream); 1415 SerialUtilities.writePaint(this.mercuryPaint, stream); 1416 SerialUtilities.writeStroke(this.subrangeIndicatorStroke, stream); 1417 SerialUtilities.writeStroke(this.rangeIndicatorStroke, stream); 1418 } 1419 1420 /** 1421 * Provides serialization support. 1422 * 1423 * @param stream the input stream. 1424 * 1425 * @throws IOException if there is an I/O error. 1426 * @throws ClassNotFoundException if there is a classpath problem. 1427 */ 1428 private void readObject(ObjectInputStream stream) throws IOException, 1429 ClassNotFoundException { 1430 stream.defaultReadObject(); 1431 this.thermometerStroke = SerialUtilities.readStroke(stream); 1432 this.thermometerPaint = SerialUtilities.readPaint(stream); 1433 this.valuePaint = SerialUtilities.readPaint(stream); 1434 this.mercuryPaint = SerialUtilities.readPaint(stream); 1435 this.subrangeIndicatorStroke = SerialUtilities.readStroke(stream); 1436 this.rangeIndicatorStroke = SerialUtilities.readStroke(stream); 1437 1438 if (this.rangeAxis != null) { 1439 this.rangeAxis.addChangeListener(this); 1440 } 1441 } 1442 1443 /** 1444 * Multiplies the range on the domain axis/axes by the specified factor. 1445 * 1446 * @param factor the zoom factor. 1447 * @param state the plot state. 1448 * @param source the source point. 1449 */ 1450 public void zoomDomainAxes(double factor, PlotRenderingInfo state, 1451 Point2D source) { 1452 // TODO: to be implemented. 1453 } 1454 1455 /** 1456 * Multiplies the range on the range axis/axes by the specified factor. 1457 * 1458 * @param factor the zoom factor. 1459 * @param state the plot state. 1460 * @param source the source point. 1461 */ 1462 public void zoomRangeAxes(double factor, PlotRenderingInfo state, 1463 Point2D source) { 1464 this.rangeAxis.resizeRange(factor); 1465 } 1466 1467 /** 1468 * This method does nothing. 1469 * 1470 * @param lowerPercent the lower percent. 1471 * @param upperPercent the upper percent. 1472 * @param state the plot state. 1473 * @param source the source point. 1474 */ 1475 public void zoomDomainAxes(double lowerPercent, double upperPercent, 1476 PlotRenderingInfo state, Point2D source) { 1477 // no domain axis to zoom 1478 } 1479 1480 /** 1481 * Zooms the range axes. 1482 * 1483 * @param lowerPercent the lower percent. 1484 * @param upperPercent the upper percent. 1485 * @param state the plot state. 1486 * @param source the source point. 1487 */ 1488 public void zoomRangeAxes(double lowerPercent, double upperPercent, 1489 PlotRenderingInfo state, Point2D source) { 1490 this.rangeAxis.zoomRange(lowerPercent, upperPercent); 1491 } 1492 1493 /** 1494 * Returns <code>false</code>. 1495 * 1496 * @return A boolean. 1497 */ 1498 public boolean isDomainZoomable() { 1499 return false; 1500 } 1501 1502 /** 1503 * Returns <code>true</code>. 1504 * 1505 * @return A boolean. 1506 */ 1507 public boolean isRangeZoomable() { 1508 return true; 1509 } 1510 1511 }