Source for org.jfree.chart.plot.ThermometerPlot

   1: /* ===========================================================
   2:  * JFreeChart : a free chart library for the Java(tm) platform
   3:  * ===========================================================
   4:  *
   5:  * (C) Copyright 2000-2007, by Object Refinery Limited and Contributors.
   6:  *
   7:  * Project Info:  http://www.jfree.org/jfreechart/index.html
   8:  *
   9:  * This library is free software; you can redistribute it and/or modify it 
  10:  * under the terms of the GNU Lesser General Public License as published by 
  11:  * the Free Software Foundation; either version 2.1 of the License, or 
  12:  * (at your option) any later version.
  13:  *
  14:  * This library is distributed in the hope that it will be useful, but 
  15:  * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 
  16:  * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public 
  17:  * License for more details.
  18:  *
  19:  * You should have received a copy of the GNU Lesser General Public
  20:  * License along with this library; if not, write to the Free Software
  21:  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, 
  22:  * USA.  
  23:  *
  24:  * [Java is a trademark or registered trademark of Sun Microsystems, Inc. 
  25:  * in the United States and other countries.]
  26:  *
  27:  * --------------------
  28:  * ThermometerPlot.java
  29:  * --------------------
  30:  *
  31:  * (C) Copyright 2000-2007, by Bryan Scott and Contributors.
  32:  *
  33:  * Original Author:  Bryan Scott (based on MeterPlot by Hari).
  34:  * Contributor(s):   David Gilbert (for Object Refinery Limited).
  35:  *                   Arnaud Lelievre;
  36:  *                   Julien Henry (see patch 1769088) (DG);
  37:  *
  38:  * Changes
  39:  * -------
  40:  * 11-Apr-2002 : Version 1, contributed by Bryan Scott;
  41:  * 15-Apr-2002 : Changed to implement VerticalValuePlot;
  42:  * 29-Apr-2002 : Added getVerticalValueAxis() method (DG);
  43:  * 25-Jun-2002 : Removed redundant imports (DG);
  44:  * 17-Sep-2002 : Reviewed with Checkstyle utility (DG);
  45:  * 18-Sep-2002 : Extensive changes made to API, to iron out bugs and 
  46:  *               inconsistencies (DG);
  47:  * 13-Oct-2002 : Corrected error datasetChanged which would generate exceptions
  48:  *               when value set to null (BRS).
  49:  * 23-Jan-2003 : Removed one constructor (DG);
  50:  * 26-Mar-2003 : Implemented Serializable (DG);
  51:  * 02-Jun-2003 : Removed test for compatible range axis (DG);
  52:  * 01-Jul-2003 : Added additional check in draw method to ensure value not 
  53:  *               null (BRS);
  54:  * 08-Sep-2003 : Added internationalization via use of properties 
  55:  *               resourceBundle (RFE 690236) (AL);
  56:  * 16-Sep-2003 : Changed ChartRenderingInfo --> PlotRenderingInfo (DG);
  57:  * 29-Sep-2003 : Updated draw to set value of cursor to non-zero and allow 
  58:  *               painting of axis.  An incomplete fix and needs to be set for 
  59:  *               left or right drawing (BRS);
  60:  * 19-Nov-2003 : Added support for value labels to be displayed left of the 
  61:  *               thermometer
  62:  * 19-Nov-2003 : Improved axis drawing (now default axis does not draw axis line
  63:  *               and is closer to the bulb).  Added support for the positioning
  64:  *               of the axis to the left or right of the bulb. (BRS);
  65:  * 03-Dec-2003 : Directly mapped deprecated setData()/getData() method to 
  66:  *               get/setDataset() (TM);
  67:  * 21-Jan-2004 : Update for renamed method in ValueAxis (DG);
  68:  * 07-Apr-2004 : Changed string width calculation (DG);
  69:  * 12-Nov-2004 : Implemented the new Zoomable interface (DG);
  70:  * 06-Jan-2004 : Added getOrientation() method (DG);
  71:  * 11-Jan-2005 : Removed deprecated code in preparation for 1.0.0 release (DG);
  72:  * 29-Mar-2005 : Fixed equals() method (DG);
  73:  * 05-May-2005 : Updated draw() method parameters (DG);
  74:  * 09-Jun-2005 : Fixed more bugs in equals() method (DG);
  75:  * 10-Jun-2005 : Fixed minor bug in setDisplayRange() method (DG);
  76:  * ------------- JFREECHART 1.0.x ---------------------------------------------
  77:  * 14-Nov-2006 : Fixed margin when drawing (DG);
  78:  * 03-May-2007 : Fixed datasetChanged() to handle null dataset, added null 
  79:  *               argument check and event notification to setRangeAxis(), 
  80:  *               added null argument check to setPadding(), setValueFont(),
  81:  *               setValuePaint(), setValueFormat() and setMercuryPaint(), 
  82:  *               deprecated get/setShowValueLines(), deprecated 
  83:  *               getMinimum/MaximumVerticalDataValue(), and fixed serialization 
  84:  *               bug (DG);
  85:  * 24-Sep-2007 : Implemented new methods in Zoomable interface (DG);
  86:  * 08-Oct-2007 : Added attributes for thermometer dimensions - see patch 1769088
  87:  *               by Julien Henry (DG);
  88:  * 
  89:  */
  90: 
  91: package org.jfree.chart.plot;
  92: 
  93: import java.awt.BasicStroke;
  94: import java.awt.Color;
  95: import java.awt.Font;
  96: import java.awt.FontMetrics;
  97: import java.awt.Graphics2D;
  98: import java.awt.Paint;
  99: import java.awt.Stroke;
 100: import java.awt.geom.Area;
 101: import java.awt.geom.Ellipse2D;
 102: import java.awt.geom.Line2D;
 103: import java.awt.geom.Point2D;
 104: import java.awt.geom.Rectangle2D;
 105: import java.awt.geom.RoundRectangle2D;
 106: import java.io.IOException;
 107: import java.io.ObjectInputStream;
 108: import java.io.ObjectOutputStream;
 109: import java.io.Serializable;
 110: import java.text.DecimalFormat;
 111: import java.text.NumberFormat;
 112: import java.util.Arrays;
 113: import java.util.ResourceBundle;
 114: 
 115: import org.jfree.chart.LegendItemCollection;
 116: import org.jfree.chart.axis.NumberAxis;
 117: import org.jfree.chart.axis.ValueAxis;
 118: import org.jfree.chart.event.PlotChangeEvent;
 119: import org.jfree.data.Range;
 120: import org.jfree.data.general.DatasetChangeEvent;
 121: import org.jfree.data.general.DefaultValueDataset;
 122: import org.jfree.data.general.ValueDataset;
 123: import org.jfree.io.SerialUtilities;
 124: import org.jfree.ui.RectangleEdge;
 125: import org.jfree.ui.RectangleInsets;
 126: import org.jfree.util.ObjectUtilities;
 127: import org.jfree.util.PaintUtilities;
 128: import org.jfree.util.UnitType;
 129: 
 130: /**
 131:  * A plot that displays a single value (from a {@link ValueDataset}) in a 
 132:  * thermometer type display.
 133:  * <p>
 134:  * This plot supports a number of options:
 135:  * <ol>
 136:  * <li>three sub-ranges which could be viewed as 'Normal', 'Warning' 
 137:  *   and 'Critical' ranges.</li>
 138:  * <li>the thermometer can be run in two modes:
 139:  *      <ul>
 140:  *      <li>fixed range, or</li>
 141:  *      <li>range adjusts to current sub-range.</li>
 142:  *      </ul>
 143:  * </li>
 144:  * <li>settable units to be displayed.</li>
 145:  * <li>settable display location for the value text.</li>
 146:  * </ol>
 147:  */
 148: public class ThermometerPlot extends Plot implements ValueAxisPlot,
 149:         Zoomable, Cloneable, Serializable {
 150: 
 151:     /** For serialization. */
 152:     private static final long serialVersionUID = 4087093313147984390L;
 153:     
 154:     /** A constant for unit type 'None'. */
 155:     public static final int UNITS_NONE = 0;
 156: 
 157:     /** A constant for unit type 'Fahrenheit'. */
 158:     public static final int UNITS_FAHRENHEIT = 1;
 159: 
 160:     /** A constant for unit type 'Celcius'. */
 161:     public static final int UNITS_CELCIUS = 2;
 162: 
 163:     /** A constant for unit type 'Kelvin'. */
 164:     public static final int UNITS_KELVIN = 3;
 165: 
 166:     /** A constant for the value label position (no label). */
 167:     public static final int NONE = 0;
 168: 
 169:     /** A constant for the value label position (right of the thermometer). */
 170:     public static final int RIGHT = 1;
 171: 
 172:     /** A constant for the value label position (left of the thermometer). */
 173:     public static final int LEFT = 2;
 174: 
 175:     /** A constant for the value label position (in the thermometer bulb). */
 176:     public static final int BULB = 3;
 177: 
 178:     /** A constant for the 'normal' range. */
 179:     public static final int NORMAL = 0;
 180: 
 181:     /** A constant for the 'warning' range. */
 182:     public static final int WARNING = 1;
 183: 
 184:     /** A constant for the 'critical' range. */
 185:     public static final int CRITICAL = 2;
 186: 
 187:     /** 
 188:      * The bulb radius. 
 189:      * 
 190:      * @deprecated As of 1.0.7, use {@link #getBulbRadius()}.
 191:      */
 192:     protected static final int BULB_RADIUS = 40;
 193: 
 194:     /** 
 195:      * The bulb diameter. 
 196:      * 
 197:      * @deprecated As of 1.0.7, use {@link #getBulbDiameter()}.
 198:      */
 199:     protected static final int BULB_DIAMETER = BULB_RADIUS * 2;
 200: 
 201:     /** 
 202:      * The column radius. 
 203:      * 
 204:      * @deprecated As of 1.0.7, use {@link #getColumnRadius()}.
 205:      */
 206:     protected static final int COLUMN_RADIUS = 20;
 207: 
 208:     /** 
 209:      * The column diameter.
 210:      * 
 211:      * @deprecated As of 1.0.7, use {@link #getColumnDiameter()}.
 212:      */
 213:     protected static final int COLUMN_DIAMETER = COLUMN_RADIUS * 2;
 214: 
 215:     /** 
 216:      * The gap radius. 
 217:      *
 218:      * @deprecated As of 1.0.7, use {@link #getGap()}.
 219:      */
 220:     protected static final int GAP_RADIUS = 5;
 221: 
 222:     /** 
 223:      * The gap diameter. 
 224:      *
 225:      * @deprecated As of 1.0.7, use {@link #getGap()} times two.
 226:      */
 227:     protected static final int GAP_DIAMETER = GAP_RADIUS * 2;
 228: 
 229:     /** The axis gap. */
 230:     protected static final int AXIS_GAP = 10;
 231: 
 232:     /** The unit strings. */
 233:     protected static final String[] UNITS = {"", "\u00B0F", "\u00B0C", 
 234:             "\u00B0K"};
 235: 
 236:     /** Index for low value in subrangeInfo matrix. */
 237:     protected static final int RANGE_LOW = 0;
 238: 
 239:     /** Index for high value in subrangeInfo matrix. */
 240:     protected static final int RANGE_HIGH = 1;
 241: 
 242:     /** Index for display low value in subrangeInfo matrix. */
 243:     protected static final int DISPLAY_LOW = 2;
 244: 
 245:     /** Index for display high value in subrangeInfo matrix. */
 246:     protected static final int DISPLAY_HIGH = 3;
 247: 
 248:     /** The default lower bound. */
 249:     protected static final double DEFAULT_LOWER_BOUND = 0.0;
 250: 
 251:     /** The default upper bound. */
 252:     protected static final double DEFAULT_UPPER_BOUND = 100.0;
 253: 
 254:     /** 
 255:      * The default bulb radius.
 256:      *
 257:      * @since 1.0.7
 258:      */
 259:     protected static final int DEFAULT_BULB_RADIUS = 40;
 260: 
 261:     /** 
 262:      * The default column radius.
 263:      *
 264:      * @since 1.0.7
 265:      */
 266:     protected static final int DEFAULT_COLUMN_RADIUS = 20;
 267: 
 268:     /** 
 269:      * The default gap between the outlines representing the thermometer.
 270:      *
 271:      * @since 1.0.7
 272:      */
 273:     protected static final int DEFAULT_GAP = 5;
 274: 
 275:     /** The dataset for the plot. */
 276:     private ValueDataset dataset;
 277: 
 278:     /** The range axis. */
 279:     private ValueAxis rangeAxis;
 280: 
 281:     /** The lower bound for the thermometer. */
 282:     private double lowerBound = DEFAULT_LOWER_BOUND;
 283: 
 284:     /** The upper bound for the thermometer. */
 285:     private double upperBound = DEFAULT_UPPER_BOUND;
 286: 
 287:     /** 
 288:      * The value label position.
 289:      *
 290:      * @since 1.0.7
 291:      */
 292:     private int bulbRadius = DEFAULT_BULB_RADIUS;
 293: 
 294:     /** 
 295:      * The column radius.
 296:      *
 297:      * @since 1.0.7
 298:      */
 299:     private int columnRadius = DEFAULT_COLUMN_RADIUS;
 300: 
 301:     /** 
 302:      * The gap between the two outlines the represent the thermometer.
 303:      *
 304:      * @since 1.0.7
 305:      */
 306:     private int gap = DEFAULT_GAP;
 307: 
 308:     /** 
 309:      * Blank space inside the plot area around the outside of the thermometer. 
 310:      */
 311:     private RectangleInsets padding;
 312: 
 313:     /** Stroke for drawing the thermometer */
 314:     private transient Stroke thermometerStroke = new BasicStroke(1.0f);
 315: 
 316:     /** Paint for drawing the thermometer */
 317:     private transient Paint thermometerPaint = Color.black;
 318: 
 319:     /** The display units */
 320:     private int units = UNITS_CELCIUS;
 321: 
 322:     /** The value label position. */
 323:     private int valueLocation = BULB;
 324: 
 325:     /** The position of the axis **/
 326:     private int axisLocation = LEFT;
 327: 
 328:     /** The font to write the value in */
 329:     private Font valueFont = new Font("SansSerif", Font.BOLD, 16);
 330: 
 331:     /** Colour that the value is written in */
 332:     private transient Paint valuePaint = Color.white;
 333: 
 334:     /** Number format for the value */
 335:     private NumberFormat valueFormat = new DecimalFormat();
 336: 
 337:     /** The default paint for the mercury in the thermometer. */
 338:     private transient Paint mercuryPaint = Color.lightGray;
 339: 
 340:     /** A flag that controls whether value lines are drawn. */
 341:     private boolean showValueLines = false;
 342: 
 343:     /** The display sub-range. */
 344:     private int subrange = -1;
 345: 
 346:     /** The start and end values for the subranges. */
 347:     private double[][] subrangeInfo = {
 348:         {0.0, 50.0, 0.0, 50.0}, 
 349:         {50.0, 75.0, 50.0, 75.0}, 
 350:         {75.0, 100.0, 75.0, 100.0}
 351:     };
 352: 
 353:     /** 
 354:      * A flag that controls whether or not the axis range adjusts to the 
 355:      * sub-ranges. 
 356:      */
 357:     private boolean followDataInSubranges = false;
 358: 
 359:     /** 
 360:      * A flag that controls whether or not the mercury paint changes with 
 361:      * the subranges. 
 362:      */
 363:     private boolean useSubrangePaint = true;
 364: 
 365:     /** Paint for each range */
 366:     private transient Paint[] subrangePaint = {Color.green, Color.orange, 
 367:             Color.red};
 368: 
 369:     /** A flag that controls whether the sub-range indicators are visible. */
 370:     private boolean subrangeIndicatorsVisible = true;
 371: 
 372:     /** The stroke for the sub-range indicators. */
 373:     private transient Stroke subrangeIndicatorStroke = new BasicStroke(2.0f);
 374: 
 375:     /** The range indicator stroke. */
 376:     private transient Stroke rangeIndicatorStroke = new BasicStroke(3.0f);
 377: 
 378:     /** The resourceBundle for the localization. */
 379:     protected static ResourceBundle localizationResources =
 380:         ResourceBundle.getBundle("org.jfree.chart.plot.LocalizationBundle");
 381: 
 382:     /**
 383:      * Creates a new thermometer plot.
 384:      */
 385:     public ThermometerPlot() {
 386:         this(new DefaultValueDataset());
 387:     }
 388: 
 389:     /**
 390:      * Creates a new thermometer plot, using default attributes where necessary.
 391:      *
 392:      * @param dataset  the data set.
 393:      */
 394:     public ThermometerPlot(ValueDataset dataset) {
 395: 
 396:         super();
 397: 
 398:         this.padding = new RectangleInsets(UnitType.RELATIVE, 0.05, 0.05, 0.05, 
 399:                 0.05);
 400:         this.dataset = dataset;
 401:         if (dataset != null) {
 402:             dataset.addChangeListener(this);
 403:         }
 404:         NumberAxis axis = new NumberAxis(null);
 405:         axis.setStandardTickUnits(NumberAxis.createIntegerTickUnits());
 406:         axis.setAxisLineVisible(false);
 407:         axis.setPlot(this);
 408:         axis.addChangeListener(this);
 409:         this.rangeAxis = axis;
 410:         setAxisRange();
 411:     }
 412: 
 413:     /**
 414:      * Returns the dataset for the plot.
 415:      *
 416:      * @return The dataset (possibly <code>null</code>).
 417:      * 
 418:      * @see #setDataset(ValueDataset)
 419:      */
 420:     public ValueDataset getDataset() {
 421:         return this.dataset;
 422:     }
 423: 
 424:     /**
 425:      * Sets the dataset for the plot, replacing the existing dataset if there 
 426:      * is one, and sends a {@link PlotChangeEvent} to all registered listeners.
 427:      *
 428:      * @param dataset  the dataset (<code>null</code> permitted).
 429:      * 
 430:      * @see #getDataset()
 431:      */
 432:     public void setDataset(ValueDataset dataset) {
 433: 
 434:         // if there is an existing dataset, remove the plot from the list 
 435:         // of change listeners...
 436:         ValueDataset existing = this.dataset;
 437:         if (existing != null) {
 438:             existing.removeChangeListener(this);
 439:         }
 440: 
 441:         // set the new dataset, and register the chart as a change listener...
 442:         this.dataset = dataset;
 443:         if (dataset != null) {
 444:             setDatasetGroup(dataset.getGroup());
 445:             dataset.addChangeListener(this);
 446:         }
 447: 
 448:         // send a dataset change event to self...
 449:         DatasetChangeEvent event = new DatasetChangeEvent(this, dataset);
 450:         datasetChanged(event);
 451: 
 452:     }
 453: 
 454:     /**
 455:      * Returns the range axis.
 456:      *
 457:      * @return The range axis (never <code>null</code>).
 458:      * 
 459:      * @see #setRangeAxis(ValueAxis)
 460:      */
 461:     public ValueAxis getRangeAxis() {
 462:         return this.rangeAxis;
 463:     }
 464: 
 465:     /**
 466:      * Sets the range axis for the plot and sends a {@link PlotChangeEvent} to 
 467:      * all registered listeners.
 468:      *
 469:      * @param axis  the new axis (<code>null</code> not permitted).
 470:      * 
 471:      * @see #getRangeAxis()
 472:      */
 473:     public void setRangeAxis(ValueAxis axis) {
 474:         if (axis == null) {
 475:             throw new IllegalArgumentException("Null 'axis' argument.");
 476:         }
 477:         // plot is registered as a listener with the existing axis...
 478:         this.rangeAxis.removeChangeListener(this);
 479: 
 480:         axis.setPlot(this);
 481:         axis.addChangeListener(this);
 482:         this.rangeAxis = axis;
 483:         notifyListeners(new PlotChangeEvent(this));
 484: 
 485:     }
 486: 
 487:     /**
 488:      * Returns the lower bound for the thermometer.  The data value can be set 
 489:      * lower than this, but it will not be shown in the thermometer.
 490:      *
 491:      * @return The lower bound.
 492:      * 
 493:      * @see #setLowerBound(double)
 494:      */
 495:     public double getLowerBound() {
 496:         return this.lowerBound;
 497:     }
 498: 
 499:     /**
 500:      * Sets the lower bound for the thermometer.
 501:      *
 502:      * @param lower the lower bound.
 503:      * 
 504:      * @see #getLowerBound()
 505:      */
 506:     public void setLowerBound(double lower) {
 507:         this.lowerBound = lower;
 508:         setAxisRange();
 509:     }
 510: 
 511:     /**
 512:      * Returns the upper bound for the thermometer.  The data value can be set 
 513:      * higher than this, but it will not be shown in the thermometer.
 514:      *
 515:      * @return The upper bound.
 516:      * 
 517:      * @see #setUpperBound(double)
 518:      */
 519:     public double getUpperBound() {
 520:         return this.upperBound;
 521:     }
 522: 
 523:     /**
 524:      * Sets the upper bound for the thermometer.
 525:      *
 526:      * @param upper the upper bound.
 527:      * 
 528:      * @see #getUpperBound()
 529:      */
 530:     public void setUpperBound(double upper) {
 531:         this.upperBound = upper;
 532:         setAxisRange();
 533:     }
 534: 
 535:     /**
 536:      * Sets the lower and upper bounds for the thermometer.
 537:      *
 538:      * @param lower  the lower bound.
 539:      * @param upper  the upper bound.
 540:      */
 541:     public void setRange(double lower, double upper) {
 542:         this.lowerBound = lower;
 543:         this.upperBound = upper;
 544:         setAxisRange();
 545:     }
 546: 
 547:     /**
 548:      * Returns the padding for the thermometer.  This is the space inside the 
 549:      * plot area.
 550:      *
 551:      * @return The padding (never <code>null</code>).
 552:      * 
 553:      * @see #setPadding(RectangleInsets)
 554:      */
 555:     public RectangleInsets getPadding() {
 556:         return this.padding;
 557:     }
 558: 
 559:     /**
 560:      * Sets the padding for the thermometer and sends a {@link PlotChangeEvent} 
 561:      * to all registered listeners.
 562:      *
 563:      * @param padding  the padding (<code>null</code> not permitted).
 564:      * 
 565:      * @see #getPadding()
 566:      */
 567:     public void setPadding(RectangleInsets padding) {
 568:         if (padding == null) {
 569:             throw new IllegalArgumentException("Null 'padding' argument.");
 570:         }
 571:         this.padding = padding;
 572:         notifyListeners(new PlotChangeEvent(this));
 573:     }
 574: 
 575:     /**
 576:      * Returns the stroke used to draw the thermometer outline.
 577:      *
 578:      * @return The stroke (never <code>null</code>).
 579:      * 
 580:      * @see #setThermometerStroke(Stroke)
 581:      * @see #getThermometerPaint()
 582:      */
 583:     public Stroke getThermometerStroke() {
 584:         return this.thermometerStroke;
 585:     }
 586: 
 587:     /**
 588:      * Sets the stroke used to draw the thermometer outline and sends a 
 589:      * {@link PlotChangeEvent} to all registered listeners.
 590:      *
 591:      * @param s  the new stroke (<code>null</code> ignored).
 592:      * 
 593:      * @see #getThermometerStroke()
 594:      */
 595:     public void setThermometerStroke(Stroke s) {
 596:         if (s != null) {
 597:             this.thermometerStroke = s;
 598:             notifyListeners(new PlotChangeEvent(this));
 599:         }
 600:     }
 601: 
 602:     /**
 603:      * Returns the paint used to draw the thermometer outline.
 604:      *
 605:      * @return The paint (never <code>null</code>).
 606:      * 
 607:      * @see #setThermometerPaint(Paint)
 608:      * @see #getThermometerStroke()
 609:      */
 610:     public Paint getThermometerPaint() {
 611:         return this.thermometerPaint;
 612:     }
 613: 
 614:     /**
 615:      * Sets the paint used to draw the thermometer outline and sends a 
 616:      * {@link PlotChangeEvent} to all registered listeners.
 617:      *
 618:      * @param paint  the new paint (<code>null</code> ignored).
 619:      * 
 620:      * @see #getThermometerPaint()
 621:      */
 622:     public void setThermometerPaint(Paint paint) {
 623:         if (paint != null) {
 624:             this.thermometerPaint = paint;
 625:             notifyListeners(new PlotChangeEvent(this));
 626:         }
 627:     }
 628: 
 629:     /**
 630:      * Returns a code indicating the unit display type.  This is one of
 631:      * {@link #UNITS_NONE}, {@link #UNITS_FAHRENHEIT}, {@link #UNITS_CELCIUS} 
 632:      * and {@link #UNITS_KELVIN}.
 633:      *
 634:      * @return The units type.
 635:      * 
 636:      * @see #setUnits(int)
 637:      */
 638:     public int getUnits() {
 639:         return this.units;
 640:     }
 641: 
 642:     /**
 643:      * Sets the units to be displayed in the thermometer. Use one of the 
 644:      * following constants:
 645:      *
 646:      * <ul>
 647:      * <li>UNITS_NONE : no units displayed.</li>
 648:      * <li>UNITS_FAHRENHEIT : units displayed in Fahrenheit.</li>
 649:      * <li>UNITS_CELCIUS : units displayed in Celcius.</li>
 650:      * <li>UNITS_KELVIN : units displayed in Kelvin.</li>
 651:      * </ul>
 652:      *
 653:      * @param u  the new unit type.
 654:      * 
 655:      * @see #getUnits()
 656:      */
 657:     public void setUnits(int u) {
 658:         if ((u >= 0) && (u < UNITS.length)) {
 659:             if (this.units != u) {
 660:                 this.units = u;
 661:                 notifyListeners(new PlotChangeEvent(this));
 662:             }
 663:         }
 664:     }
 665: 
 666:     /**
 667:      * Sets the unit type.
 668:      *
 669:      * @param u  the unit type (<code>null</code> ignored).
 670:      * 
 671:      * @deprecated Use setUnits(int) instead.  Deprecated as of version 1.0.6,
 672:      *     because this method is a little obscure and redundant anyway.
 673:      */
 674:     public void setUnits(String u) {
 675:         if (u == null) {
 676:             return;
 677:         }
 678: 
 679:         u = u.toUpperCase().trim();
 680:         for (int i = 0; i < UNITS.length; ++i) {
 681:             if (u.equals(UNITS[i].toUpperCase().trim())) {
 682:                 setUnits(i);
 683:                 i = UNITS.length;
 684:             }
 685:         }
 686:     }
 687: 
 688:     /**
 689:      * Returns a code indicating the location at which the value label is
 690:      * displayed.
 691:      *
 692:      * @return The location (one of {@link #NONE}, {@link #RIGHT}, 
 693:      *         {@link #LEFT} and {@link #BULB}.).
 694:      */
 695:     public int getValueLocation() {
 696:         return this.valueLocation;
 697:     }
 698: 
 699:     /**
 700:      * Sets the location at which the current value is displayed and sends a
 701:      * {@link PlotChangeEvent} to all registered listeners.
 702:      * <P>
 703:      * The location can be one of the constants:
 704:      * <code>NONE</code>,
 705:      * <code>RIGHT</code>
 706:      * <code>LEFT</code> and
 707:      * <code>BULB</code>.
 708:      *
 709:      * @param location  the location.
 710:      */
 711:     public void setValueLocation(int location) {
 712:         if ((location >= 0) && (location < 4)) {
 713:             this.valueLocation = location;
 714:             notifyListeners(new PlotChangeEvent(this));
 715:         }
 716:         else {
 717:             throw new IllegalArgumentException("Location not recognised.");
 718:         }
 719:     }
 720: 
 721:     /**
 722:      * Returns the axis location.
 723:      *
 724:      * @return The location (one of {@link #NONE}, {@link #LEFT} and 
 725:      *         {@link #RIGHT}).
 726:      *         
 727:      * @see #setAxisLocation(int)
 728:      */
 729:     public int getAxisLocation() {
 730:         return this.axisLocation;
 731:     }
 732: 
 733:     /**
 734:      * Sets the location at which the axis is displayed relative to the 
 735:      * thermometer, and sends a {@link PlotChangeEvent} to all registered
 736:      * listeners.
 737:      *
 738:      * @param location  the location (one of {@link #NONE}, {@link #LEFT} and 
 739:      *         {@link #RIGHT}).
 740:      * 
 741:      * @see #getAxisLocation()
 742:      */
 743:     public void setAxisLocation(int location) {
 744:         if ((location >= 0) && (location < 3)) {
 745:             this.axisLocation = location;
 746:             notifyListeners(new PlotChangeEvent(this));
 747:         }
 748:         else {
 749:             throw new IllegalArgumentException("Location not recognised.");
 750:         }
 751:     }
 752: 
 753:     /**
 754:      * Gets the font used to display the current value.
 755:      *
 756:      * @return The font.
 757:      * 
 758:      * @see #setValueFont(Font)
 759:      */
 760:     public Font getValueFont() {
 761:         return this.valueFont;
 762:     }
 763: 
 764:     /**
 765:      * Sets the font used to display the current value.
 766:      *
 767:      * @param f  the new font (<code>null</code> not permitted).
 768:      * 
 769:      * @see #getValueFont()
 770:      */
 771:     public void setValueFont(Font f) {
 772:         if (f == null) {
 773:             throw new IllegalArgumentException("Null 'font' argument.");
 774:         }
 775:         if (!this.valueFont.equals(f)) {
 776:             this.valueFont = f;
 777:             notifyListeners(new PlotChangeEvent(this));
 778:         }
 779:     }
 780: 
 781:     /**
 782:      * Gets the paint used to display the current value.
 783:     *
 784:      * @return The paint.
 785:      * 
 786:      * @see #setValuePaint(Paint)
 787:      */
 788:     public Paint getValuePaint() {
 789:         return this.valuePaint;
 790:     }
 791: 
 792:     /**
 793:      * Sets the paint used to display the current value and sends a 
 794:      * {@link PlotChangeEvent} to all registered listeners.
 795:      *
 796:      * @param paint  the new paint (<code>null</code> not permitted).
 797:      * 
 798:      * @see #getValuePaint()
 799:      */
 800:     public void setValuePaint(Paint paint) {
 801:         if (paint == null) {
 802:             throw new IllegalArgumentException("Null 'paint' argument.");
 803:         }
 804:         if (!this.valuePaint.equals(paint)) {
 805:             this.valuePaint = paint;
 806:             notifyListeners(new PlotChangeEvent(this));
 807:         }
 808:     }
 809: 
 810:     // FIXME: No getValueFormat() method?
 811:     
 812:     /**
 813:      * Sets the formatter for the value label and sends a 
 814:      * {@link PlotChangeEvent} to all registered listeners.
 815:      *
 816:      * @param formatter  the new formatter (<code>null</code> not permitted).
 817:      */
 818:     public void setValueFormat(NumberFormat formatter) {
 819:         if (formatter == null) {
 820:             throw new IllegalArgumentException("Null 'formatter' argument.");
 821:         }
 822:         this.valueFormat = formatter;
 823:         notifyListeners(new PlotChangeEvent(this));
 824:     }
 825: 
 826:     /**
 827:      * Returns the default mercury paint.
 828:      *
 829:      * @return The paint (never <code>null</code>).
 830:      * 
 831:      * @see #setMercuryPaint(Paint)
 832:      */
 833:     public Paint getMercuryPaint() {
 834:         return this.mercuryPaint;
 835:     }
 836: 
 837:     /**
 838:      * Sets the default mercury paint and sends a {@link PlotChangeEvent} to 
 839:      * all registered listeners.
 840:      *
 841:      * @param paint  the new paint (<code>null</code> not permitted).
 842:      * 
 843:      * @see #getMercuryPaint()
 844:      */
 845:     public void setMercuryPaint(Paint paint) {
 846:         if (paint == null) {
 847:             throw new IllegalArgumentException("Null 'paint' argument.");
 848:         }
 849:         this.mercuryPaint = paint;
 850:         notifyListeners(new PlotChangeEvent(this));
 851:     }
 852: 
 853:     /**
 854:      * Returns the flag that controls whether not value lines are displayed.
 855:      *
 856:      * @return The flag.
 857:      * 
 858:      * @see #setShowValueLines(boolean)
 859:      * 
 860:      * @deprecated This flag doesn't do anything useful/visible.  Deprecated 
 861:      *     as of version 1.0.6.
 862:      */
 863:     public boolean getShowValueLines() {
 864:         return this.showValueLines;
 865:     }
 866: 
 867:     /**
 868:      * Sets the display as to whether to show value lines in the output.
 869:      *
 870:      * @param b Whether to show value lines in the thermometer
 871:      * 
 872:      * @see #getShowValueLines()
 873:      * 
 874:      * @deprecated This flag doesn't do anything useful/visible.  Deprecated 
 875:      *     as of version 1.0.6.
 876:      */
 877:     public void setShowValueLines(boolean b) {
 878:         this.showValueLines = b;
 879:         notifyListeners(new PlotChangeEvent(this));
 880:     }
 881: 
 882:     /**
 883:      * Sets information for a particular range.
 884:      *
 885:      * @param range  the range to specify information about.
 886:      * @param low  the low value for the range
 887:      * @param hi  the high value for the range
 888:      */
 889:     public void setSubrangeInfo(int range, double low, double hi) {
 890:         setSubrangeInfo(range, low, hi, low, hi);
 891:     }
 892: 
 893:     /**
 894:      * Sets the subrangeInfo attribute of the ThermometerPlot object
 895:      *
 896:      * @param range  the new rangeInfo value.
 897:      * @param rangeLow  the new rangeInfo value
 898:      * @param rangeHigh  the new rangeInfo value
 899:      * @param displayLow  the new rangeInfo value
 900:      * @param displayHigh  the new rangeInfo value
 901:      */
 902:     public void setSubrangeInfo(int range,
 903:                                 double rangeLow, double rangeHigh,
 904:                                 double displayLow, double displayHigh) {
 905: 
 906:         if ((range >= 0) && (range < 3)) {
 907:             setSubrange(range, rangeLow, rangeHigh);
 908:             setDisplayRange(range, displayLow, displayHigh);
 909:             setAxisRange();
 910:             notifyListeners(new PlotChangeEvent(this));
 911:         }
 912: 
 913:     }
 914: 
 915:     /**
 916:      * Sets the bounds for a subrange.
 917:      *
 918:      * @param range  the range type.
 919:      * @param low  the low value.
 920:      * @param high  the high value.
 921:      */
 922:     public void setSubrange(int range, double low, double high) {
 923:         if ((range >= 0) && (range < 3)) {
 924:             this.subrangeInfo[range][RANGE_HIGH] = high;
 925:             this.subrangeInfo[range][RANGE_LOW] = low;
 926:         }
 927:     }
 928: 
 929:     /**
 930:      * Sets the displayed bounds for a sub range.
 931:      *
 932:      * @param range  the range type.
 933:      * @param low  the low value.
 934:      * @param high  the high value.
 935:      */
 936:     public void setDisplayRange(int range, double low, double high) {
 937: 
 938:         if ((range >= 0) && (range < this.subrangeInfo.length)
 939:             && isValidNumber(high) && isValidNumber(low)) {
 940:  
 941:             if (high > low) {
 942:                 this.subrangeInfo[range][DISPLAY_HIGH] = high;
 943:                 this.subrangeInfo[range][DISPLAY_LOW] = low;
 944:             }
 945:             else {
 946:                 this.subrangeInfo[range][DISPLAY_HIGH] = low;
 947:                 this.subrangeInfo[range][DISPLAY_LOW] = high;
 948:             }
 949: 
 950:         }
 951: 
 952:     }
 953: 
 954:     /**
 955:      * Gets the paint used for a particular subrange.
 956:      *
 957:      * @param range  the range (.
 958:      *
 959:      * @return The paint.
 960:      * 
 961:      * @see #setSubrangePaint(int, Paint)
 962:      */
 963:     public Paint getSubrangePaint(int range) {
 964:         if ((range >= 0) && (range < this.subrangePaint.length)) {
 965:             return this.subrangePaint[range];
 966:         }
 967:         else {
 968:             return this.mercuryPaint;
 969:         }
 970:     }
 971: 
 972:     /**
 973:      * Sets the paint to be used for a subrange and sends a 
 974:      * {@link PlotChangeEvent} to all registered listeners.
 975:      *
 976:      * @param range  the range (0, 1 or 2).
 977:      * @param paint  the paint to be applied (<code>null</code> not permitted).
 978:      * 
 979:      * @see #getSubrangePaint(int)
 980:      */
 981:     public void setSubrangePaint(int range, Paint paint) {
 982:         if ((range >= 0) 
 983:                 && (range < this.subrangePaint.length) && (paint != null)) {
 984:             this.subrangePaint[range] = paint;
 985:             notifyListeners(new PlotChangeEvent(this));
 986:         }
 987:     }
 988: 
 989:     /**
 990:      * Returns a flag that controls whether or not the thermometer axis zooms 
 991:      * to display the subrange within which the data value falls.
 992:      *
 993:      * @return The flag.
 994:      */
 995:     public boolean getFollowDataInSubranges() {
 996:         return this.followDataInSubranges;
 997:     }
 998: 
 999:     /**
1000:      * Sets the flag that controls whether or not the thermometer axis zooms 
1001:      * to display the subrange within which the data value falls.
1002:      *
1003:      * @param flag  the flag.
1004:      */
1005:     public void setFollowDataInSubranges(boolean flag) {
1006:         this.followDataInSubranges = flag;
1007:         notifyListeners(new PlotChangeEvent(this));
1008:     }
1009: 
1010:     /**
1011:      * Returns a flag that controls whether or not the mercury color changes 
1012:      * for each subrange.
1013:      *
1014:      * @return The flag.
1015:      * 
1016:      * @see #setUseSubrangePaint(boolean)
1017:      */
1018:     public boolean getUseSubrangePaint() {
1019:         return this.useSubrangePaint;
1020:     }
1021: 
1022:     /**
1023:      * Sets the range colour change option.
1024:      *
1025:      * @param flag the new range colour change option
1026:      * 
1027:      * @see #getUseSubrangePaint()
1028:      */
1029:     public void setUseSubrangePaint(boolean flag) {
1030:         this.useSubrangePaint = flag;
1031:         notifyListeners(new PlotChangeEvent(this));
1032:     }
1033: 
1034:     /**
1035:      * Returns the bulb radius, in Java2D units.
1036: 
1037:      * @return The bulb radius.
1038:      * 
1039:      * @since 1.0.7
1040:      */
1041:     public int getBulbRadius() {
1042:         return this.bulbRadius;
1043:     }
1044: 
1045:     /**
1046:      * Sets the bulb radius (in Java2D units) and sends a 
1047:      * {@link PlotChangeEvent} to all registered listeners.
1048:      * 
1049:      * @param r  the new radius (in Java2D units).
1050:      * 
1051:      * @see #getBulbRadius()
1052:      * 
1053:      * @since 1.0.7
1054:      */
1055:     public void setBulbRadius(int r) {
1056:         this.bulbRadius = r;
1057:         notifyListeners(new PlotChangeEvent(this));
1058:     }
1059: 
1060:     /**
1061:      * Returns the bulb diameter, which is always twice the value returned
1062:      * by {@link #getBulbRadius()}.
1063:      * 
1064:      * @return The bulb diameter.
1065:      * 
1066:      * @since 1.0.7
1067:      */
1068:     public int getBulbDiameter() {
1069:         return getBulbRadius() * 2;
1070:     }
1071: 
1072:     /**
1073:      * Returns the column radius, in Java2D units.
1074:      * 
1075:      * @return The column radius.
1076:      * 
1077:      * @see #setColumnRadius(int)
1078:      * 
1079:      * @since 1.0.7
1080:      */
1081:     public int getColumnRadius() {
1082:         return this.columnRadius;
1083:     }
1084: 
1085:     /**
1086:      * Sets the column radius (in Java2D units) and sends a 
1087:      * {@link PlotChangeEvent} to all registered listeners.
1088:      * 
1089:      * @param r  the new radius.
1090:      * 
1091:      * @see #getColumnRadius()
1092:      * 
1093:      * @since 1.0.7
1094:      */
1095:     public void setColumnRadius(int r) {
1096:         this.columnRadius = r;
1097:         notifyListeners(new PlotChangeEvent(this));
1098:     }
1099: 
1100:     /**
1101:      * Returns the column diameter, which is always twice the value returned
1102:      * by {@link #getColumnRadius()}.
1103:      * 
1104:      * @return The column diameter.
1105:      * 
1106:      * @since 1.0.7
1107:      */
1108:     public int getColumnDiameter() {
1109:         return getColumnRadius() * 2;
1110:     }
1111: 
1112:     /**
1113:      * Returns the gap, in Java2D units, between the two outlines that 
1114:      * represent the thermometer.
1115:      * 
1116:      * @return The gap.
1117:      * 
1118:      * @see #setGap(int)
1119:      * 
1120:      * @since 1.0.7
1121:      */
1122:     public int getGap() {
1123:         return this.gap;
1124:     }
1125: 
1126:     /**
1127:      * Sets the gap (in Java2D units) between the two outlines that represent
1128:      * the thermometer, and sends a {@link PlotChangeEvent} to all registered 
1129:      * listeners.
1130:      * 
1131:      * @param gap  the new gap.
1132:      * 
1133:      * @see #getGap()
1134:      * 
1135:      * @since 1.0.7
1136:      */
1137:     public void setGap(int gap) {
1138:         this.gap = gap;
1139:         notifyListeners(new PlotChangeEvent(this));
1140:     }
1141: 
1142:     /**
1143:      * Draws the plot on a Java 2D graphics device (such as the screen or a 
1144:      * printer).
1145:      *
1146:      * @param g2  the graphics device.
1147:      * @param area  the area within which the plot should be drawn.
1148:      * @param anchor  the anchor point (<code>null</code> permitted).
1149:      * @param parentState  the state from the parent plot, if there is one.
1150:      * @param info  collects info about the drawing.
1151:      */
1152:     public void draw(Graphics2D g2, Rectangle2D area, Point2D anchor,
1153:                      PlotState parentState,
1154:                      PlotRenderingInfo info) {
1155: 
1156:         RoundRectangle2D outerStem = new RoundRectangle2D.Double();
1157:         RoundRectangle2D innerStem = new RoundRectangle2D.Double();
1158:         RoundRectangle2D mercuryStem = new RoundRectangle2D.Double();
1159:         Ellipse2D outerBulb = new Ellipse2D.Double();
1160:         Ellipse2D innerBulb = new Ellipse2D.Double();
1161:         String temp = null;
1162:         FontMetrics metrics = null;
1163:         if (info != null) {
1164:             info.setPlotArea(area);
1165:         }
1166: 
1167:         // adjust for insets...
1168:         RectangleInsets insets = getInsets();
1169:         insets.trim(area);
1170:         drawBackground(g2, area);
1171: 
1172:         // adjust for padding...
1173:         Rectangle2D interior = (Rectangle2D) area.clone();
1174:         this.padding.trim(interior);
1175:         int midX = (int) (interior.getX() + (interior.getWidth() / 2));
1176:         int midY = (int) (interior.getY() + (interior.getHeight() / 2));
1177:         int stemTop = (int) (interior.getMinY() + getBulbRadius());
1178:         int stemBottom = (int) (interior.getMaxY() - getBulbDiameter());
1179:         Rectangle2D dataArea = new Rectangle2D.Double(midX - getColumnRadius(), 
1180:                 stemTop, getColumnRadius(), stemBottom - stemTop);
1181: 
1182:         outerBulb.setFrame(midX - getBulbRadius(), stemBottom, 
1183:                 getBulbDiameter(), getBulbDiameter());
1184: 
1185:         outerStem.setRoundRect(midX - getColumnRadius(), interior.getMinY(), 
1186:                 getColumnDiameter(), stemBottom + getBulbDiameter() - stemTop,
1187:                 getColumnDiameter(), getColumnDiameter());
1188: 
1189:         Area outerThermometer = new Area(outerBulb);
1190:         Area tempArea = new Area(outerStem);
1191:         outerThermometer.add(tempArea);
1192: 
1193:         innerBulb.setFrame(midX - getBulbRadius() + getGap(), stemBottom 
1194:                 + getGap(), getBulbDiameter() - getGap() * 2, getBulbDiameter()
1195:                 - getGap() * 2);
1196: 
1197:         innerStem.setRoundRect(midX - getColumnRadius() + getGap(), 
1198:                 interior.getMinY() + getGap(), getColumnDiameter() 
1199:                 - getGap() * 2, stemBottom + getBulbDiameter() - getGap() * 2 
1200:                 - stemTop, getColumnDiameter() - getGap() * 2, 
1201:                 getColumnDiameter() - getGap() * 2);
1202: 
1203:         Area innerThermometer = new Area(innerBulb);
1204:         tempArea = new Area(innerStem);
1205:         innerThermometer.add(tempArea);
1206:    
1207:         if ((this.dataset != null) && (this.dataset.getValue() != null)) {
1208:             double current = this.dataset.getValue().doubleValue();
1209:             double ds = this.rangeAxis.valueToJava2D(current, dataArea, 
1210:                     RectangleEdge.LEFT);
1211: 
1212:             int i = getColumnDiameter() - getGap() * 2; // already calculated
1213:             int j = getColumnRadius() - getGap(); // already calculated
1214:             int l = (i / 2);
1215:             int k = (int) Math.round(ds);
1216:             if (k < (getGap() + interior.getMinY())) {
1217:                 k = (int) (getGap() + interior.getMinY());
1218:                 l = getBulbRadius();
1219:             }
1220: 
1221:             Area mercury = new Area(innerBulb);
1222: 
1223:             if (k < (stemBottom + getBulbRadius())) {
1224:                 mercuryStem.setRoundRect(midX - j, k, i, 
1225:                         (stemBottom + getBulbRadius()) - k, l, l);
1226:                 tempArea = new Area(mercuryStem);
1227:                 mercury.add(tempArea);
1228:             }
1229: 
1230:             g2.setPaint(getCurrentPaint());
1231:             g2.fill(mercury);
1232: 
1233:             // draw range indicators...
1234:             if (this.subrangeIndicatorsVisible) {
1235:                 g2.setStroke(this.subrangeIndicatorStroke);
1236:                 Range range = this.rangeAxis.getRange();
1237: 
1238:                 // draw start of normal range
1239:                 double value = this.subrangeInfo[NORMAL][RANGE_LOW];
1240:                 if (range.contains(value)) {
1241:                     double x = midX + getColumnRadius() + 2;
1242:                     double y = this.rangeAxis.valueToJava2D(value, dataArea, 
1243:                             RectangleEdge.LEFT);
1244:                     Line2D line = new Line2D.Double(x, y, x + 10, y);
1245:                     g2.setPaint(this.subrangePaint[NORMAL]);
1246:                     g2.draw(line);
1247:                 }
1248: 
1249:                 // draw start of warning range
1250:                 value = this.subrangeInfo[WARNING][RANGE_LOW];
1251:                 if (range.contains(value)) {
1252:                     double x = midX + getColumnRadius() + 2;
1253:                     double y = this.rangeAxis.valueToJava2D(value, dataArea, 
1254:                             RectangleEdge.LEFT);
1255:                     Line2D line = new Line2D.Double(x, y, x + 10, y);
1256:                     g2.setPaint(this.subrangePaint[WARNING]);
1257:                     g2.draw(line);
1258:                 }
1259: 
1260:                 // draw start of critical range
1261:                 value = this.subrangeInfo[CRITICAL][RANGE_LOW];
1262:                 if (range.contains(value)) {
1263:                     double x = midX + getColumnRadius() + 2;
1264:                     double y = this.rangeAxis.valueToJava2D(value, dataArea, 
1265:                             RectangleEdge.LEFT);
1266:                     Line2D line = new Line2D.Double(x, y, x + 10, y);
1267:                     g2.setPaint(this.subrangePaint[CRITICAL]);
1268:                     g2.draw(line);
1269:                 }
1270:             }
1271: 
1272:             // draw the axis...
1273:             if ((this.rangeAxis != null) && (this.axisLocation != NONE)) {
1274:                 int drawWidth = AXIS_GAP;
1275:                 if (this.showValueLines) {
1276:                     drawWidth += getColumnDiameter();
1277:                 }
1278:                 Rectangle2D drawArea;
1279:                 double cursor = 0;
1280: 
1281:                 switch (this.axisLocation) {
1282:                     case RIGHT:
1283:                         cursor = midX + getColumnRadius();
1284:                         drawArea = new Rectangle2D.Double(cursor,
1285:                                 stemTop, drawWidth, (stemBottom - stemTop + 1));
1286:                         this.rangeAxis.draw(g2, cursor, area, drawArea, 
1287:                                 RectangleEdge.RIGHT, null);
1288:                         break;
1289: 
1290:                     case LEFT:
1291:                     default:
1292:                         //cursor = midX - COLUMN_RADIUS - AXIS_GAP;
1293:                         cursor = midX - getColumnRadius();
1294:                         drawArea = new Rectangle2D.Double(cursor, stemTop,
1295:                                 drawWidth, (stemBottom - stemTop + 1));
1296:                         this.rangeAxis.draw(g2, cursor, area, drawArea, 
1297:                                 RectangleEdge.LEFT, null);
1298:                         break;
1299:                 }
1300:                    
1301:             }
1302: 
1303:             // draw text value on screen
1304:             g2.setFont(this.valueFont);
1305:             g2.setPaint(this.valuePaint);
1306:             metrics = g2.getFontMetrics();
1307:             switch (this.valueLocation) {
1308:                 case RIGHT:
1309:                     g2.drawString(this.valueFormat.format(current), 
1310:                             midX + getColumnRadius() + getGap(), midY);
1311:                     break;
1312:                 case LEFT:
1313:                     String valueString = this.valueFormat.format(current);
1314:                     int stringWidth = metrics.stringWidth(valueString);
1315:                     g2.drawString(valueString, midX - getColumnRadius() 
1316:                             - getGap() - stringWidth, midY);
1317:                     break;
1318:                 case BULB:
1319:                     temp = this.valueFormat.format(current);
1320:                     i = metrics.stringWidth(temp) / 2;
1321:                     g2.drawString(temp, midX - i, 
1322:                             stemBottom + getBulbRadius() + getGap());
1323:                     break;
1324:                 default:
1325:             }
1326:             /***/
1327:         }
1328: 
1329:         g2.setPaint(this.thermometerPaint);
1330:         g2.setFont(this.valueFont);
1331: 
1332:         //  draw units indicator
1333:         metrics = g2.getFontMetrics();
1334:         int tickX1 = midX - getColumnRadius() - getGap() * 2
1335:                      - metrics.stringWidth(UNITS[this.units]);
1336:         if (tickX1 > area.getMinX()) {
1337:             g2.drawString(UNITS[this.units], tickX1, 
1338:                     (int) (area.getMinY() + 20));
1339:         }
1340: 
1341:         // draw thermometer outline
1342:         g2.setStroke(this.thermometerStroke);
1343:         g2.draw(outerThermometer);
1344:         g2.draw(innerThermometer);
1345: 
1346:         drawOutline(g2, area);
1347:     }
1348: 
1349:     /**
1350:      * A zoom method that does nothing.  Plots are required to support the 
1351:      * zoom operation.  In the case of a thermometer chart, it doesn't make 
1352:      * sense to zoom in or out, so the method is empty.
1353:      *
1354:      * @param percent  the zoom percentage.
1355:      */
1356:     public void zoom(double percent) {
1357:         // intentionally blank
1358:    }
1359: 
1360:     /**
1361:      * Returns a short string describing the type of plot.
1362:      *
1363:      * @return A short string describing the type of plot.
1364:      */
1365:     public String getPlotType() {
1366:         return localizationResources.getString("Thermometer_Plot");
1367:     }
1368: 
1369:     /**
1370:      * Checks to see if a new value means the axis range needs adjusting.
1371:      *
1372:      * @param event  the dataset change event.
1373:      */
1374:     public void datasetChanged(DatasetChangeEvent event) {
1375:         if (this.dataset != null) {
1376:             Number vn = this.dataset.getValue();
1377:             if (vn != null) {
1378:                 double value = vn.doubleValue();
1379:                 if (inSubrange(NORMAL, value)) {
1380:                     this.subrange = NORMAL;
1381:                 }
1382:                 else if (inSubrange(WARNING, value)) {
1383:                    this.subrange = WARNING;
1384:                 }
1385:                 else if (inSubrange(CRITICAL, value)) {
1386:                     this.subrange = CRITICAL;
1387:                 }
1388:                 else {
1389:                     this.subrange = -1;
1390:                 }
1391:                 setAxisRange();
1392:             }
1393:         }
1394:         super.datasetChanged(event);
1395:     }
1396: 
1397:     /**
1398:      * Returns the minimum value in either the domain or the range, whichever
1399:      * is displayed against the vertical axis for the particular type of plot
1400:      * implementing this interface.
1401:      *
1402:      * @return The minimum value in either the domain or the range.
1403:      * 
1404:      * @deprecated This method is not used.  Officially deprecated in version 
1405:      *         1.0.6.
1406:      */
1407:     public Number getMinimumVerticalDataValue() {
1408:         return new Double(this.lowerBound);
1409:     }
1410: 
1411:     /**
1412:      * Returns the maximum value in either the domain or the range, whichever
1413:      * is displayed against the vertical axis for the particular type of plot
1414:      * implementing this interface.
1415:      *
1416:      * @return The maximum value in either the domain or the range
1417:      * 
1418:      * @deprecated This method is not used.  Officially deprecated in version 
1419:      *         1.0.6.
1420:      */
1421:     public Number getMaximumVerticalDataValue() {
1422:         return new Double(this.upperBound);
1423:     }
1424: 
1425:     /**
1426:      * Returns the data range.
1427:      *
1428:      * @param axis  the axis.
1429:      *
1430:      * @return The range of data displayed.
1431:      */
1432:     public Range getDataRange(ValueAxis axis) {
1433:        return new Range(this.lowerBound, this.upperBound);
1434:     }
1435: 
1436:     /**
1437:      * Sets the axis range to the current values in the rangeInfo array.
1438:      */
1439:     protected void setAxisRange() {
1440:         if ((this.subrange >= 0) && (this.followDataInSubranges)) {
1441:             this.rangeAxis.setRange(
1442:                     new Range(this.subrangeInfo[this.subrange][DISPLAY_LOW],
1443:                     this.subrangeInfo[this.subrange][DISPLAY_HIGH]));
1444:         }
1445:         else {
1446:             this.rangeAxis.setRange(this.lowerBound, this.upperBound);
1447:         }
1448:     }
1449: 
1450:     /**
1451:      * Returns the legend items for the plot.
1452:      *
1453:      * @return <code>null</code>.
1454:      */
1455:     public LegendItemCollection getLegendItems() {
1456:         return null;
1457:     }
1458: 
1459:     /**
1460:      * Returns the orientation of the plot.
1461:      * 
1462:      * @return The orientation (always {@link PlotOrientation#VERTICAL}).
1463:      */
1464:     public PlotOrientation getOrientation() {
1465:         return PlotOrientation.VERTICAL;    
1466:     }
1467: 
1468:     /**
1469:      * Determine whether a number is valid and finite.
1470:      *
1471:      * @param d  the number to be tested.
1472:      *
1473:      * @return <code>true</code> if the number is valid and finite, and 
1474:      *         <code>false</code> otherwise.
1475:      */
1476:     protected static boolean isValidNumber(double d) {
1477:         return (!(Double.isNaN(d) || Double.isInfinite(d)));
1478:     }
1479: 
1480:     /**
1481:      * Returns true if the value is in the specified range, and false otherwise.
1482:      *
1483:      * @param subrange  the subrange.
1484:      * @param value  the value to check.
1485:      *
1486:      * @return A boolean.
1487:      */
1488:     private boolean inSubrange(int subrange, double value) {
1489:         return (value > this.subrangeInfo[subrange][RANGE_LOW]
1490:             && value <= this.subrangeInfo[subrange][RANGE_HIGH]);
1491:     }
1492: 
1493:     /**
1494:      * Returns the mercury paint corresponding to the current data value.
1495:      * Called from the {@link #draw(Graphics2D, Rectangle2D, Point2D, 
1496:      * PlotState, PlotRenderingInfo)} method.
1497:      *
1498:      * @return The paint (never <code>null</code>).
1499:      */
1500:     private Paint getCurrentPaint() {
1501:         Paint result = this.mercuryPaint;
1502:         if (this.useSubrangePaint) {
1503:             double value = this.dataset.getValue().doubleValue();
1504:             if (inSubrange(NORMAL, value)) {
1505:                 result = this.subrangePaint[NORMAL];
1506:             }
1507:             else if (inSubrange(WARNING, value)) {
1508:                 result = this.subrangePaint[WARNING];
1509:             }
1510:             else if (inSubrange(CRITICAL, value)) {
1511:                 result = this.subrangePaint[CRITICAL];
1512:             }
1513:         }
1514:         return result;
1515:     }
1516: 
1517:     /**
1518:      * Tests this plot for equality with another object.  The plot's dataset
1519:      * is not considered in the test.
1520:      *
1521:      * @param obj  the object (<code>null</code> permitted).
1522:      *
1523:      * @return <code>true</code> or <code>false</code>.
1524:      */
1525:     public boolean equals(Object obj) {
1526:         if (obj == this) {
1527:             return true;
1528:         }
1529:         if (!(obj instanceof ThermometerPlot)) {
1530:             return false;
1531:         }
1532:         ThermometerPlot that = (ThermometerPlot) obj;
1533:         if (!super.equals(obj)) {
1534:             return false;
1535:         }
1536:         if (!ObjectUtilities.equal(this.rangeAxis, that.rangeAxis)) {
1537:             return false;
1538:         }
1539:         if (this.axisLocation != that.axisLocation) {
1540:             return false;   
1541:         }
1542:         if (this.lowerBound != that.lowerBound) {
1543:             return false;
1544:         }
1545:         if (this.upperBound != that.upperBound) {
1546:             return false;
1547:         }
1548:         if (!ObjectUtilities.equal(this.padding, that.padding)) {
1549:             return false;
1550:         }
1551:         if (!ObjectUtilities.equal(this.thermometerStroke, 
1552:                 that.thermometerStroke)) {
1553:             return false;
1554:         }
1555:         if (!PaintUtilities.equal(this.thermometerPaint, 
1556:                 that.thermometerPaint)) {
1557:             return false;
1558:         }
1559:         if (this.units != that.units) {
1560:             return false;
1561:         }
1562:         if (this.valueLocation != that.valueLocation) {
1563:             return false;
1564:         }
1565:         if (!ObjectUtilities.equal(this.valueFont, that.valueFont)) {
1566:             return false;
1567:         }
1568:         if (!PaintUtilities.equal(this.valuePaint, that.valuePaint)) {
1569:             return false;
1570:         }
1571:         if (!ObjectUtilities.equal(this.valueFormat, that.valueFormat)) {
1572:             return false;
1573:         }
1574:         if (!PaintUtilities.equal(this.mercuryPaint, that.mercuryPaint)) {
1575:             return false;
1576:         }
1577:         if (this.showValueLines != that.showValueLines) {
1578:             return false;
1579:         }
1580:         if (this.subrange != that.subrange) {
1581:             return false;
1582:         }
1583:         if (this.followDataInSubranges != that.followDataInSubranges) {
1584:             return false;
1585:         }
1586:         if (!equal(this.subrangeInfo, that.subrangeInfo)) {
1587:             return false;   
1588:         }
1589:         if (this.useSubrangePaint != that.useSubrangePaint) {
1590:             return false;
1591:         }
1592:         if (this.bulbRadius != that.bulbRadius) {
1593:             return false;
1594:         }
1595:         if (this.columnRadius != that.columnRadius) {
1596:             return false;
1597:         }
1598:         if (this.gap != that.gap) {
1599:             return false;
1600:         }
1601:         for (int i = 0; i < this.subrangePaint.length; i++) {
1602:             if (!PaintUtilities.equal(this.subrangePaint[i], 
1603:                     that.subrangePaint[i])) {
1604:                 return false;   
1605:             }
1606:         }
1607:         return true;
1608:     }
1609: 
1610:     /**
1611:      * Tests two double[][] arrays for equality.
1612:      * 
1613:      * @param array1  the first array (<code>null</code> permitted).
1614:      * @param array2  the second arrray (<code>null</code> permitted).
1615:      * 
1616:      * @return A boolean.
1617:      */
1618:     private static boolean equal(double[][] array1, double[][] array2) {
1619:         if (array1 == null) {
1620:             return (array2 == null);
1621:         }
1622:         if (array2 == null) {
1623:             return false;
1624:         }
1625:         if (array1.length != array2.length) {
1626:             return false;
1627:         }
1628:         for (int i = 0; i < array1.length; i++) {
1629:             if (!Arrays.equals(array1[i], array2[i])) {
1630:                 return false;
1631:             }
1632:         }
1633:         return true;
1634:     }
1635: 
1636:     /**
1637:      * Returns a clone of the plot.
1638:      *
1639:      * @return A clone.
1640:      *
1641:      * @throws CloneNotSupportedException  if the plot cannot be cloned.
1642:      */
1643:     public Object clone() throws CloneNotSupportedException {
1644: 
1645:         ThermometerPlot clone = (ThermometerPlot) super.clone();
1646: 
1647:         if (clone.dataset != null) {
1648:             clone.dataset.addChangeListener(clone);
1649:         }
1650:         clone.rangeAxis = (ValueAxis) ObjectUtilities.clone(this.rangeAxis);
1651:         if (clone.rangeAxis != null) {
1652:             clone.rangeAxis.setPlot(clone);
1653:             clone.rangeAxis.addChangeListener(clone);
1654:         }
1655:         clone.valueFormat = (NumberFormat) this.valueFormat.clone();
1656:         clone.subrangePaint = (Paint[]) this.subrangePaint.clone();
1657: 
1658:         return clone;
1659: 
1660:     }
1661: 
1662:     /**
1663:      * Provides serialization support.
1664:      *
1665:      * @param stream  the output stream.
1666:      *
1667:      * @throws IOException  if there is an I/O error.
1668:      */
1669:     private void writeObject(ObjectOutputStream stream) throws IOException { 
1670:         stream.defaultWriteObject();
1671:         SerialUtilities.writeStroke(this.thermometerStroke, stream);
1672:         SerialUtilities.writePaint(this.thermometerPaint, stream);
1673:         SerialUtilities.writePaint(this.valuePaint, stream);
1674:         SerialUtilities.writePaint(this.mercuryPaint, stream);
1675:         SerialUtilities.writeStroke(this.subrangeIndicatorStroke, stream);
1676:         SerialUtilities.writeStroke(this.rangeIndicatorStroke, stream);
1677:         for (int i = 0; i < 3; i++) {
1678:             SerialUtilities.writePaint(this.subrangePaint[i], stream);
1679:         }
1680:     }
1681: 
1682:     /**
1683:      * Provides serialization support.
1684:      *
1685:      * @param stream  the input stream.
1686:      *
1687:      * @throws IOException  if there is an I/O error.
1688:      * @throws ClassNotFoundException  if there is a classpath problem.
1689:      */
1690:     private void readObject(ObjectInputStream stream) throws IOException,
1691:             ClassNotFoundException {
1692:         stream.defaultReadObject();
1693:         this.thermometerStroke = SerialUtilities.readStroke(stream);
1694:         this.thermometerPaint = SerialUtilities.readPaint(stream);
1695:         this.valuePaint = SerialUtilities.readPaint(stream);
1696:         this.mercuryPaint = SerialUtilities.readPaint(stream);
1697:         this.subrangeIndicatorStroke = SerialUtilities.readStroke(stream);
1698:         this.rangeIndicatorStroke = SerialUtilities.readStroke(stream);
1699:         this.subrangePaint = new Paint[3];
1700:         for (int i = 0; i < 3; i++) {
1701:             this.subrangePaint[i] = SerialUtilities.readPaint(stream);
1702:         }
1703:         if (this.rangeAxis != null) {
1704:             this.rangeAxis.addChangeListener(this);
1705:         }
1706:     }
1707: 
1708:     /**
1709:      * Multiplies the range on the domain axis/axes by the specified factor.
1710:      *
1711:      * @param factor  the zoom factor.
1712:      * @param state  the plot state.
1713:      * @param source  the source point.
1714:      */
1715:     public void zoomDomainAxes(double factor, PlotRenderingInfo state, 
1716:                                Point2D source) {
1717:         // no domain axis to zoom
1718:     }
1719: 
1720:     /**
1721:      * Multiplies the range on the domain axis/axes by the specified factor.
1722:      *
1723:      * @param factor  the zoom factor.
1724:      * @param state  the plot state.
1725:      * @param source  the source point.
1726:      * @param useAnchor  a flag that controls whether or not the source point
1727:      *         is used for the zoom anchor.
1728:      *         
1729:      * @since 1.0.7
1730:      */
1731:     public void zoomDomainAxes(double factor, PlotRenderingInfo state, 
1732:                                Point2D source, boolean useAnchor) {
1733:         // no domain axis to zoom
1734:     }
1735:     
1736:     /**
1737:      * Multiplies the range on the range axis/axes by the specified factor.
1738:      *
1739:      * @param factor  the zoom factor.
1740:      * @param state  the plot state.
1741:      * @param source  the source point.
1742:      */
1743:     public void zoomRangeAxes(double factor, PlotRenderingInfo state, 
1744:                               Point2D source) {
1745:         this.rangeAxis.resizeRange(factor);
1746:     }
1747: 
1748:     /**
1749:      * Multiplies the range on the range axis/axes by the specified factor.
1750:      *
1751:      * @param factor  the zoom factor.
1752:      * @param state  the plot state.
1753:      * @param source  the source point.
1754:      * @param useAnchor  a flag that controls whether or not the source point
1755:      *         is used for the zoom anchor.
1756:      *         
1757:      * @since 1.0.7
1758:      */
1759:     public void zoomRangeAxes(double factor, PlotRenderingInfo state, 
1760:                               Point2D source, boolean useAnchor) {
1761:         double anchorY = this.getRangeAxis().java2DToValue(source.getY(), 
1762:                 state.getDataArea(), RectangleEdge.LEFT);
1763:         this.rangeAxis.resizeRange(factor, anchorY);
1764:     }
1765:     
1766:     /**
1767:      * This method does nothing.
1768:      *
1769:      * @param lowerPercent  the lower percent.
1770:      * @param upperPercent  the upper percent.
1771:      * @param state  the plot state.
1772:      * @param source  the source point.
1773:      */
1774:     public void zoomDomainAxes(double lowerPercent, double upperPercent, 
1775:                                PlotRenderingInfo state, Point2D source) {
1776:         // no domain axis to zoom
1777:     }
1778: 
1779:     /**
1780:      * Zooms the range axes.
1781:      *
1782:      * @param lowerPercent  the lower percent.
1783:      * @param upperPercent  the upper percent.
1784:      * @param state  the plot state.
1785:      * @param source  the source point.
1786:      */
1787:     public void zoomRangeAxes(double lowerPercent, double upperPercent, 
1788:                               PlotRenderingInfo state, Point2D source) {
1789:         this.rangeAxis.zoomRange(lowerPercent, upperPercent);
1790:     }
1791:   
1792:     /**
1793:      * Returns <code>false</code>.
1794:      * 
1795:      * @return A boolean.
1796:      */
1797:     public boolean isDomainZoomable() {
1798:         return false;
1799:     }
1800:     
1801:     /**
1802:      * Returns <code>true</code>.
1803:      * 
1804:      * @return A boolean.
1805:      */
1806:     public boolean isRangeZoomable() {
1807:         return true;
1808:     }
1809: 
1810: }