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    
091    package org.jfree.chart.plot;
092    
093    import java.awt.BasicStroke;
094    import java.awt.Color;
095    import java.awt.Font;
096    import java.awt.FontMetrics;
097    import java.awt.Graphics2D;
098    import java.awt.Paint;
099    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    }