001/* ===========================================================
002 * JFreeChart : a free chart library for the Java(tm) platform
003 * ===========================================================
004 *
005 * (C) Copyright 2000-2007, by Object Refinery Limited and Contributors.
006 *
007 * Project Info:  http://www.jfree.org/jfreechart/index.html
008 *
009 * This library is free software; you can redistribute it and/or modify it 
010 * under the terms of the GNU Lesser General Public License as published by 
011 * the Free Software Foundation; either version 2.1 of the License, or 
012 * (at your option) any later version.
013 *
014 * This library is distributed in the hope that it will be useful, but 
015 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 
016 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public 
017 * License for more details.
018 *
019 * You should have received a copy of the GNU Lesser General Public
020 * License along with this library; if not, write to the Free Software
021 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, 
022 * USA.  
023 *
024 * [Java is a trademark or registered trademark of Sun Microsystems, Inc. 
025 * in the United States and other countries.]
026 *
027 * --------------------
028 * ThermometerPlot.java
029 * --------------------
030 *
031 * (C) Copyright 2000-2007, by Bryan Scott and Contributors.
032 *
033 * Original Author:  Bryan Scott (based on MeterPlot by Hari).
034 * Contributor(s):   David Gilbert (for Object Refinery Limited).
035 *                   Arnaud Lelievre;
036 *                   Julien Henry (see patch 1769088) (DG);
037 *
038 * Changes
039 * -------
040 * 11-Apr-2002 : Version 1, contributed by Bryan Scott;
041 * 15-Apr-2002 : Changed to implement VerticalValuePlot;
042 * 29-Apr-2002 : Added getVerticalValueAxis() method (DG);
043 * 25-Jun-2002 : Removed redundant imports (DG);
044 * 17-Sep-2002 : Reviewed with Checkstyle utility (DG);
045 * 18-Sep-2002 : Extensive changes made to API, to iron out bugs and 
046 *               inconsistencies (DG);
047 * 13-Oct-2002 : Corrected error datasetChanged which would generate exceptions
048 *               when value set to null (BRS).
049 * 23-Jan-2003 : Removed one constructor (DG);
050 * 26-Mar-2003 : Implemented Serializable (DG);
051 * 02-Jun-2003 : Removed test for compatible range axis (DG);
052 * 01-Jul-2003 : Added additional check in draw method to ensure value not 
053 *               null (BRS);
054 * 08-Sep-2003 : Added internationalization via use of properties 
055 *               resourceBundle (RFE 690236) (AL);
056 * 16-Sep-2003 : Changed ChartRenderingInfo --> PlotRenderingInfo (DG);
057 * 29-Sep-2003 : Updated draw to set value of cursor to non-zero and allow 
058 *               painting of axis.  An incomplete fix and needs to be set for 
059 *               left or right drawing (BRS);
060 * 19-Nov-2003 : Added support for value labels to be displayed left of the 
061 *               thermometer
062 * 19-Nov-2003 : Improved axis drawing (now default axis does not draw axis line
063 *               and is closer to the bulb).  Added support for the positioning
064 *               of the axis to the left or right of the bulb. (BRS);
065 * 03-Dec-2003 : Directly mapped deprecated setData()/getData() method to 
066 *               get/setDataset() (TM);
067 * 21-Jan-2004 : Update for renamed method in ValueAxis (DG);
068 * 07-Apr-2004 : Changed string width calculation (DG);
069 * 12-Nov-2004 : Implemented the new Zoomable interface (DG);
070 * 06-Jan-2004 : Added getOrientation() method (DG);
071 * 11-Jan-2005 : Removed deprecated code in preparation for 1.0.0 release (DG);
072 * 29-Mar-2005 : Fixed equals() method (DG);
073 * 05-May-2005 : Updated draw() method parameters (DG);
074 * 09-Jun-2005 : Fixed more bugs in equals() method (DG);
075 * 10-Jun-2005 : Fixed minor bug in setDisplayRange() method (DG);
076 * ------------- JFREECHART 1.0.x ---------------------------------------------
077 * 14-Nov-2006 : Fixed margin when drawing (DG);
078 * 03-May-2007 : Fixed datasetChanged() to handle null dataset, added null 
079 *               argument check and event notification to setRangeAxis(), 
080 *               added null argument check to setPadding(), setValueFont(),
081 *               setValuePaint(), setValueFormat() and setMercuryPaint(), 
082 *               deprecated get/setShowValueLines(), deprecated 
083 *               getMinimum/MaximumVerticalDataValue(), and fixed serialization 
084 *               bug (DG);
085 * 24-Sep-2007 : Implemented new methods in Zoomable interface (DG);
086 * 08-Oct-2007 : Added attributes for thermometer dimensions - see patch 1769088
087 *               by Julien Henry (DG);
088 * 
089 */
090
091package org.jfree.chart.plot;
092
093import java.awt.BasicStroke;
094import java.awt.Color;
095import java.awt.Font;
096import java.awt.FontMetrics;
097import java.awt.Graphics2D;
098import java.awt.Paint;
099import java.awt.Stroke;
100import java.awt.geom.Area;
101import java.awt.geom.Ellipse2D;
102import java.awt.geom.Line2D;
103import java.awt.geom.Point2D;
104import java.awt.geom.Rectangle2D;
105import java.awt.geom.RoundRectangle2D;
106import java.io.IOException;
107import java.io.ObjectInputStream;
108import java.io.ObjectOutputStream;
109import java.io.Serializable;
110import java.text.DecimalFormat;
111import java.text.NumberFormat;
112import java.util.Arrays;
113import java.util.ResourceBundle;
114
115import org.jfree.chart.LegendItemCollection;
116import org.jfree.chart.axis.NumberAxis;
117import org.jfree.chart.axis.ValueAxis;
118import org.jfree.chart.event.PlotChangeEvent;
119import org.jfree.data.Range;
120import org.jfree.data.general.DatasetChangeEvent;
121import org.jfree.data.general.DefaultValueDataset;
122import org.jfree.data.general.ValueDataset;
123import org.jfree.io.SerialUtilities;
124import org.jfree.ui.RectangleEdge;
125import org.jfree.ui.RectangleInsets;
126import org.jfree.util.ObjectUtilities;
127import org.jfree.util.PaintUtilities;
128import 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 */
148public 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}