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            fireChangeEvent();
484        }
485    
486        /**
487         * Returns the lower bound for the thermometer.  The data value can be set 
488         * lower than this, but it will not be shown in the thermometer.
489         *
490         * @return The lower bound.
491         * 
492         * @see #setLowerBound(double)
493         */
494        public double getLowerBound() {
495            return this.lowerBound;
496        }
497    
498        /**
499         * Sets the lower bound for the thermometer.
500         *
501         * @param lower the lower bound.
502         * 
503         * @see #getLowerBound()
504         */
505        public void setLowerBound(double lower) {
506            this.lowerBound = lower;
507            setAxisRange();
508        }
509    
510        /**
511         * Returns the upper bound for the thermometer.  The data value can be set 
512         * higher than this, but it will not be shown in the thermometer.
513         *
514         * @return The upper bound.
515         * 
516         * @see #setUpperBound(double)
517         */
518        public double getUpperBound() {
519            return this.upperBound;
520        }
521    
522        /**
523         * Sets the upper bound for the thermometer.
524         *
525         * @param upper the upper bound.
526         * 
527         * @see #getUpperBound()
528         */
529        public void setUpperBound(double upper) {
530            this.upperBound = upper;
531            setAxisRange();
532        }
533    
534        /**
535         * Sets the lower and upper bounds for the thermometer.
536         *
537         * @param lower  the lower bound.
538         * @param upper  the upper bound.
539         */
540        public void setRange(double lower, double upper) {
541            this.lowerBound = lower;
542            this.upperBound = upper;
543            setAxisRange();
544        }
545    
546        /**
547         * Returns the padding for the thermometer.  This is the space inside the 
548         * plot area.
549         *
550         * @return The padding (never <code>null</code>).
551         * 
552         * @see #setPadding(RectangleInsets)
553         */
554        public RectangleInsets getPadding() {
555            return this.padding;
556        }
557    
558        /**
559         * Sets the padding for the thermometer and sends a {@link PlotChangeEvent} 
560         * to all registered listeners.
561         *
562         * @param padding  the padding (<code>null</code> not permitted).
563         * 
564         * @see #getPadding()
565         */
566        public void setPadding(RectangleInsets padding) {
567            if (padding == null) {
568                throw new IllegalArgumentException("Null 'padding' argument.");
569            }
570            this.padding = padding;
571            fireChangeEvent();
572        }
573    
574        /**
575         * Returns the stroke used to draw the thermometer outline.
576         *
577         * @return The stroke (never <code>null</code>).
578         * 
579         * @see #setThermometerStroke(Stroke)
580         * @see #getThermometerPaint()
581         */
582        public Stroke getThermometerStroke() {
583            return this.thermometerStroke;
584        }
585    
586        /**
587         * Sets the stroke used to draw the thermometer outline and sends a 
588         * {@link PlotChangeEvent} to all registered listeners.
589         *
590         * @param s  the new stroke (<code>null</code> ignored).
591         * 
592         * @see #getThermometerStroke()
593         */
594        public void setThermometerStroke(Stroke s) {
595            if (s != null) {
596                this.thermometerStroke = s;
597                fireChangeEvent();
598            }
599        }
600    
601        /**
602         * Returns the paint used to draw the thermometer outline.
603         *
604         * @return The paint (never <code>null</code>).
605         * 
606         * @see #setThermometerPaint(Paint)
607         * @see #getThermometerStroke()
608         */
609        public Paint getThermometerPaint() {
610            return this.thermometerPaint;
611        }
612    
613        /**
614         * Sets the paint used to draw the thermometer outline and sends a 
615         * {@link PlotChangeEvent} to all registered listeners.
616         *
617         * @param paint  the new paint (<code>null</code> ignored).
618         * 
619         * @see #getThermometerPaint()
620         */
621        public void setThermometerPaint(Paint paint) {
622            if (paint != null) {
623                this.thermometerPaint = paint;
624                fireChangeEvent();
625            }
626        }
627    
628        /**
629         * Returns a code indicating the unit display type.  This is one of
630         * {@link #UNITS_NONE}, {@link #UNITS_FAHRENHEIT}, {@link #UNITS_CELCIUS} 
631         * and {@link #UNITS_KELVIN}.
632         *
633         * @return The units type.
634         * 
635         * @see #setUnits(int)
636         */
637        public int getUnits() {
638            return this.units;
639        }
640    
641        /**
642         * Sets the units to be displayed in the thermometer. Use one of the 
643         * following constants:
644         *
645         * <ul>
646         * <li>UNITS_NONE : no units displayed.</li>
647         * <li>UNITS_FAHRENHEIT : units displayed in Fahrenheit.</li>
648         * <li>UNITS_CELCIUS : units displayed in Celcius.</li>
649         * <li>UNITS_KELVIN : units displayed in Kelvin.</li>
650         * </ul>
651         *
652         * @param u  the new unit type.
653         * 
654         * @see #getUnits()
655         */
656        public void setUnits(int u) {
657            if ((u >= 0) && (u < UNITS.length)) {
658                if (this.units != u) {
659                    this.units = u;
660                    fireChangeEvent();
661                }
662            }
663        }
664    
665        /**
666         * Sets the unit type.
667         *
668         * @param u  the unit type (<code>null</code> ignored).
669         * 
670         * @deprecated Use setUnits(int) instead.  Deprecated as of version 1.0.6,
671         *     because this method is a little obscure and redundant anyway.
672         */
673        public void setUnits(String u) {
674            if (u == null) {
675                return;
676            }
677    
678            u = u.toUpperCase().trim();
679            for (int i = 0; i < UNITS.length; ++i) {
680                if (u.equals(UNITS[i].toUpperCase().trim())) {
681                    setUnits(i);
682                    i = UNITS.length;
683                }
684            }
685        }
686    
687        /**
688         * Returns a code indicating the location at which the value label is
689         * displayed.
690         *
691         * @return The location (one of {@link #NONE}, {@link #RIGHT}, 
692         *         {@link #LEFT} and {@link #BULB}.).
693         */
694        public int getValueLocation() {
695            return this.valueLocation;
696        }
697    
698        /**
699         * Sets the location at which the current value is displayed and sends a
700         * {@link PlotChangeEvent} to all registered listeners.
701         * <P>
702         * The location can be one of the constants:
703         * <code>NONE</code>,
704         * <code>RIGHT</code>
705         * <code>LEFT</code> and
706         * <code>BULB</code>.
707         *
708         * @param location  the location.
709         */
710        public void setValueLocation(int location) {
711            if ((location >= 0) && (location < 4)) {
712                this.valueLocation = location;
713                fireChangeEvent();
714            }
715            else {
716                throw new IllegalArgumentException("Location not recognised.");
717            }
718        }
719    
720        /**
721         * Returns the axis location.
722         *
723         * @return The location (one of {@link #NONE}, {@link #LEFT} and 
724         *         {@link #RIGHT}).
725         *         
726         * @see #setAxisLocation(int)
727         */
728        public int getAxisLocation() {
729            return this.axisLocation;
730        }
731    
732        /**
733         * Sets the location at which the axis is displayed relative to the 
734         * thermometer, and sends a {@link PlotChangeEvent} to all registered
735         * listeners.
736         *
737         * @param location  the location (one of {@link #NONE}, {@link #LEFT} and 
738         *         {@link #RIGHT}).
739         * 
740         * @see #getAxisLocation()
741         */
742        public void setAxisLocation(int location) {
743            if ((location >= 0) && (location < 3)) {
744                this.axisLocation = location;
745                fireChangeEvent();
746            }
747            else {
748                throw new IllegalArgumentException("Location not recognised.");
749            }
750        }
751    
752        /**
753         * Gets the font used to display the current value.
754         *
755         * @return The font.
756         * 
757         * @see #setValueFont(Font)
758         */
759        public Font getValueFont() {
760            return this.valueFont;
761        }
762    
763        /**
764         * Sets the font used to display the current value.
765         *
766         * @param f  the new font (<code>null</code> not permitted).
767         * 
768         * @see #getValueFont()
769         */
770        public void setValueFont(Font f) {
771            if (f == null) {
772                throw new IllegalArgumentException("Null 'font' argument.");
773            }
774            if (!this.valueFont.equals(f)) {
775                this.valueFont = f;
776                fireChangeEvent();
777            }
778        }
779    
780        /**
781         * Gets the paint used to display the current value.
782        *
783         * @return The paint.
784         * 
785         * @see #setValuePaint(Paint)
786         */
787        public Paint getValuePaint() {
788            return this.valuePaint;
789        }
790    
791        /**
792         * Sets the paint used to display the current value and sends a 
793         * {@link PlotChangeEvent} to all registered listeners.
794         *
795         * @param paint  the new paint (<code>null</code> not permitted).
796         * 
797         * @see #getValuePaint()
798         */
799        public void setValuePaint(Paint paint) {
800            if (paint == null) {
801                throw new IllegalArgumentException("Null 'paint' argument.");
802            }
803            if (!this.valuePaint.equals(paint)) {
804                this.valuePaint = paint;
805                fireChangeEvent();
806            }
807        }
808    
809        // FIXME: No getValueFormat() method?
810        
811        /**
812         * Sets the formatter for the value label and sends a 
813         * {@link PlotChangeEvent} to all registered listeners.
814         *
815         * @param formatter  the new formatter (<code>null</code> not permitted).
816         */
817        public void setValueFormat(NumberFormat formatter) {
818            if (formatter == null) {
819                throw new IllegalArgumentException("Null 'formatter' argument.");
820            }
821            this.valueFormat = formatter;
822            fireChangeEvent();
823        }
824    
825        /**
826         * Returns the default mercury paint.
827         *
828         * @return The paint (never <code>null</code>).
829         * 
830         * @see #setMercuryPaint(Paint)
831         */
832        public Paint getMercuryPaint() {
833            return this.mercuryPaint;
834        }
835    
836        /**
837         * Sets the default mercury paint and sends a {@link PlotChangeEvent} to 
838         * all registered listeners.
839         *
840         * @param paint  the new paint (<code>null</code> not permitted).
841         * 
842         * @see #getMercuryPaint()
843         */
844        public void setMercuryPaint(Paint paint) {
845            if (paint == null) {
846                throw new IllegalArgumentException("Null 'paint' argument.");
847            }
848            this.mercuryPaint = paint;
849            fireChangeEvent();
850        }
851    
852        /**
853         * Returns the flag that controls whether not value lines are displayed.
854         *
855         * @return The flag.
856         * 
857         * @see #setShowValueLines(boolean)
858         * 
859         * @deprecated This flag doesn't do anything useful/visible.  Deprecated 
860         *     as of version 1.0.6.
861         */
862        public boolean getShowValueLines() {
863            return this.showValueLines;
864        }
865    
866        /**
867         * Sets the display as to whether to show value lines in the output.
868         *
869         * @param b Whether to show value lines in the thermometer
870         * 
871         * @see #getShowValueLines()
872         * 
873         * @deprecated This flag doesn't do anything useful/visible.  Deprecated 
874         *     as of version 1.0.6.
875         */
876        public void setShowValueLines(boolean b) {
877            this.showValueLines = b;
878            fireChangeEvent();
879        }
880    
881        /**
882         * Sets information for a particular range.
883         *
884         * @param range  the range to specify information about.
885         * @param low  the low value for the range
886         * @param hi  the high value for the range
887         */
888        public void setSubrangeInfo(int range, double low, double hi) {
889            setSubrangeInfo(range, low, hi, low, hi);
890        }
891    
892        /**
893         * Sets the subrangeInfo attribute of the ThermometerPlot object
894         *
895         * @param range  the new rangeInfo value.
896         * @param rangeLow  the new rangeInfo value
897         * @param rangeHigh  the new rangeInfo value
898         * @param displayLow  the new rangeInfo value
899         * @param displayHigh  the new rangeInfo value
900         */
901        public void setSubrangeInfo(int range,
902                                    double rangeLow, double rangeHigh,
903                                    double displayLow, double displayHigh) {
904    
905            if ((range >= 0) && (range < 3)) {
906                setSubrange(range, rangeLow, rangeHigh);
907                setDisplayRange(range, displayLow, displayHigh);
908                setAxisRange();
909                fireChangeEvent();
910            }
911    
912        }
913    
914        /**
915         * Sets the bounds for a subrange.
916         *
917         * @param range  the range type.
918         * @param low  the low value.
919         * @param high  the high value.
920         */
921        public void setSubrange(int range, double low, double high) {
922            if ((range >= 0) && (range < 3)) {
923                this.subrangeInfo[range][RANGE_HIGH] = high;
924                this.subrangeInfo[range][RANGE_LOW] = low;
925            }
926        }
927    
928        /**
929         * Sets the displayed bounds for a sub range.
930         *
931         * @param range  the range type.
932         * @param low  the low value.
933         * @param high  the high value.
934         */
935        public void setDisplayRange(int range, double low, double high) {
936    
937            if ((range >= 0) && (range < this.subrangeInfo.length)
938                && isValidNumber(high) && isValidNumber(low)) {
939     
940                if (high > low) {
941                    this.subrangeInfo[range][DISPLAY_HIGH] = high;
942                    this.subrangeInfo[range][DISPLAY_LOW] = low;
943                }
944                else {
945                    this.subrangeInfo[range][DISPLAY_HIGH] = low;
946                    this.subrangeInfo[range][DISPLAY_LOW] = high;
947                }
948    
949            }
950    
951        }
952    
953        /**
954         * Gets the paint used for a particular subrange.
955         *
956         * @param range  the range (.
957         *
958         * @return The paint.
959         * 
960         * @see #setSubrangePaint(int, Paint)
961         */
962        public Paint getSubrangePaint(int range) {
963            if ((range >= 0) && (range < this.subrangePaint.length)) {
964                return this.subrangePaint[range];
965            }
966            else {
967                return this.mercuryPaint;
968            }
969        }
970    
971        /**
972         * Sets the paint to be used for a subrange and sends a 
973         * {@link PlotChangeEvent} to all registered listeners.
974         *
975         * @param range  the range (0, 1 or 2).
976         * @param paint  the paint to be applied (<code>null</code> not permitted).
977         * 
978         * @see #getSubrangePaint(int)
979         */
980        public void setSubrangePaint(int range, Paint paint) {
981            if ((range >= 0) 
982                    && (range < this.subrangePaint.length) && (paint != null)) {
983                this.subrangePaint[range] = paint;
984                fireChangeEvent();
985            }
986        }
987    
988        /**
989         * Returns a flag that controls whether or not the thermometer axis zooms 
990         * to display the subrange within which the data value falls.
991         *
992         * @return The flag.
993         */
994        public boolean getFollowDataInSubranges() {
995            return this.followDataInSubranges;
996        }
997    
998        /**
999         * Sets the flag that controls whether or not the thermometer axis zooms 
1000         * to display the subrange within which the data value falls.
1001         *
1002         * @param flag  the flag.
1003         */
1004        public void setFollowDataInSubranges(boolean flag) {
1005            this.followDataInSubranges = flag;
1006            fireChangeEvent();
1007        }
1008    
1009        /**
1010         * Returns a flag that controls whether or not the mercury color changes 
1011         * for each subrange.
1012         *
1013         * @return The flag.
1014         * 
1015         * @see #setUseSubrangePaint(boolean)
1016         */
1017        public boolean getUseSubrangePaint() {
1018            return this.useSubrangePaint;
1019        }
1020    
1021        /**
1022         * Sets the range colour change option.
1023         *
1024         * @param flag the new range colour change option
1025         * 
1026         * @see #getUseSubrangePaint()
1027         */
1028        public void setUseSubrangePaint(boolean flag) {
1029            this.useSubrangePaint = flag;
1030            fireChangeEvent();
1031        }
1032    
1033        /**
1034         * Returns the bulb radius, in Java2D units.
1035    
1036         * @return The bulb radius.
1037         * 
1038         * @since 1.0.7
1039         */
1040        public int getBulbRadius() {
1041            return this.bulbRadius;
1042        }
1043    
1044        /**
1045         * Sets the bulb radius (in Java2D units) and sends a 
1046         * {@link PlotChangeEvent} to all registered listeners.
1047         * 
1048         * @param r  the new radius (in Java2D units).
1049         * 
1050         * @see #getBulbRadius()
1051         * 
1052         * @since 1.0.7
1053         */
1054        public void setBulbRadius(int r) {
1055            this.bulbRadius = r;
1056            fireChangeEvent();
1057        }
1058    
1059        /**
1060         * Returns the bulb diameter, which is always twice the value returned
1061         * by {@link #getBulbRadius()}.
1062         * 
1063         * @return The bulb diameter.
1064         * 
1065         * @since 1.0.7
1066         */
1067        public int getBulbDiameter() {
1068            return getBulbRadius() * 2;
1069        }
1070    
1071        /**
1072         * Returns the column radius, in Java2D units.
1073         * 
1074         * @return The column radius.
1075         * 
1076         * @see #setColumnRadius(int)
1077         * 
1078         * @since 1.0.7
1079         */
1080        public int getColumnRadius() {
1081            return this.columnRadius;
1082        }
1083    
1084        /**
1085         * Sets the column radius (in Java2D units) and sends a 
1086         * {@link PlotChangeEvent} to all registered listeners.
1087         * 
1088         * @param r  the new radius.
1089         * 
1090         * @see #getColumnRadius()
1091         * 
1092         * @since 1.0.7
1093         */
1094        public void setColumnRadius(int r) {
1095            this.columnRadius = r;
1096            fireChangeEvent();
1097        }
1098    
1099        /**
1100         * Returns the column diameter, which is always twice the value returned
1101         * by {@link #getColumnRadius()}.
1102         * 
1103         * @return The column diameter.
1104         * 
1105         * @since 1.0.7
1106         */
1107        public int getColumnDiameter() {
1108            return getColumnRadius() * 2;
1109        }
1110    
1111        /**
1112         * Returns the gap, in Java2D units, between the two outlines that 
1113         * represent the thermometer.
1114         * 
1115         * @return The gap.
1116         * 
1117         * @see #setGap(int)
1118         * 
1119         * @since 1.0.7
1120         */
1121        public int getGap() {
1122            return this.gap;
1123        }
1124    
1125        /**
1126         * Sets the gap (in Java2D units) between the two outlines that represent
1127         * the thermometer, and sends a {@link PlotChangeEvent} to all registered 
1128         * listeners.
1129         * 
1130         * @param gap  the new gap.
1131         * 
1132         * @see #getGap()
1133         * 
1134         * @since 1.0.7
1135         */
1136        public void setGap(int gap) {
1137            this.gap = gap;
1138            fireChangeEvent();
1139        }
1140    
1141        /**
1142         * Draws the plot on a Java 2D graphics device (such as the screen or a 
1143         * printer).
1144         *
1145         * @param g2  the graphics device.
1146         * @param area  the area within which the plot should be drawn.
1147         * @param anchor  the anchor point (<code>null</code> permitted).
1148         * @param parentState  the state from the parent plot, if there is one.
1149         * @param info  collects info about the drawing.
1150         */
1151        public void draw(Graphics2D g2, Rectangle2D area, Point2D anchor,
1152                         PlotState parentState,
1153                         PlotRenderingInfo info) {
1154    
1155            RoundRectangle2D outerStem = new RoundRectangle2D.Double();
1156            RoundRectangle2D innerStem = new RoundRectangle2D.Double();
1157            RoundRectangle2D mercuryStem = new RoundRectangle2D.Double();
1158            Ellipse2D outerBulb = new Ellipse2D.Double();
1159            Ellipse2D innerBulb = new Ellipse2D.Double();
1160            String temp = null;
1161            FontMetrics metrics = null;
1162            if (info != null) {
1163                info.setPlotArea(area);
1164            }
1165    
1166            // adjust for insets...
1167            RectangleInsets insets = getInsets();
1168            insets.trim(area);
1169            drawBackground(g2, area);
1170    
1171            // adjust for padding...
1172            Rectangle2D interior = (Rectangle2D) area.clone();
1173            this.padding.trim(interior);
1174            int midX = (int) (interior.getX() + (interior.getWidth() / 2));
1175            int midY = (int) (interior.getY() + (interior.getHeight() / 2));
1176            int stemTop = (int) (interior.getMinY() + getBulbRadius());
1177            int stemBottom = (int) (interior.getMaxY() - getBulbDiameter());
1178            Rectangle2D dataArea = new Rectangle2D.Double(midX - getColumnRadius(), 
1179                    stemTop, getColumnRadius(), stemBottom - stemTop);
1180    
1181            outerBulb.setFrame(midX - getBulbRadius(), stemBottom, 
1182                    getBulbDiameter(), getBulbDiameter());
1183    
1184            outerStem.setRoundRect(midX - getColumnRadius(), interior.getMinY(), 
1185                    getColumnDiameter(), stemBottom + getBulbDiameter() - stemTop,
1186                    getColumnDiameter(), getColumnDiameter());
1187    
1188            Area outerThermometer = new Area(outerBulb);
1189            Area tempArea = new Area(outerStem);
1190            outerThermometer.add(tempArea);
1191    
1192            innerBulb.setFrame(midX - getBulbRadius() + getGap(), stemBottom 
1193                    + getGap(), getBulbDiameter() - getGap() * 2, getBulbDiameter()
1194                    - getGap() * 2);
1195    
1196            innerStem.setRoundRect(midX - getColumnRadius() + getGap(), 
1197                    interior.getMinY() + getGap(), getColumnDiameter() 
1198                    - getGap() * 2, stemBottom + getBulbDiameter() - getGap() * 2 
1199                    - stemTop, getColumnDiameter() - getGap() * 2, 
1200                    getColumnDiameter() - getGap() * 2);
1201    
1202            Area innerThermometer = new Area(innerBulb);
1203            tempArea = new Area(innerStem);
1204            innerThermometer.add(tempArea);
1205       
1206            if ((this.dataset != null) && (this.dataset.getValue() != null)) {
1207                double current = this.dataset.getValue().doubleValue();
1208                double ds = this.rangeAxis.valueToJava2D(current, dataArea, 
1209                        RectangleEdge.LEFT);
1210    
1211                int i = getColumnDiameter() - getGap() * 2; // already calculated
1212                int j = getColumnRadius() - getGap(); // already calculated
1213                int l = (i / 2);
1214                int k = (int) Math.round(ds);
1215                if (k < (getGap() + interior.getMinY())) {
1216                    k = (int) (getGap() + interior.getMinY());
1217                    l = getBulbRadius();
1218                }
1219    
1220                Area mercury = new Area(innerBulb);
1221    
1222                if (k < (stemBottom + getBulbRadius())) {
1223                    mercuryStem.setRoundRect(midX - j, k, i, 
1224                            (stemBottom + getBulbRadius()) - k, l, l);
1225                    tempArea = new Area(mercuryStem);
1226                    mercury.add(tempArea);
1227                }
1228    
1229                g2.setPaint(getCurrentPaint());
1230                g2.fill(mercury);
1231    
1232                // draw range indicators...
1233                if (this.subrangeIndicatorsVisible) {
1234                    g2.setStroke(this.subrangeIndicatorStroke);
1235                    Range range = this.rangeAxis.getRange();
1236    
1237                    // draw start of normal range
1238                    double value = this.subrangeInfo[NORMAL][RANGE_LOW];
1239                    if (range.contains(value)) {
1240                        double x = midX + getColumnRadius() + 2;
1241                        double y = this.rangeAxis.valueToJava2D(value, dataArea, 
1242                                RectangleEdge.LEFT);
1243                        Line2D line = new Line2D.Double(x, y, x + 10, y);
1244                        g2.setPaint(this.subrangePaint[NORMAL]);
1245                        g2.draw(line);
1246                    }
1247    
1248                    // draw start of warning range
1249                    value = this.subrangeInfo[WARNING][RANGE_LOW];
1250                    if (range.contains(value)) {
1251                        double x = midX + getColumnRadius() + 2;
1252                        double y = this.rangeAxis.valueToJava2D(value, dataArea, 
1253                                RectangleEdge.LEFT);
1254                        Line2D line = new Line2D.Double(x, y, x + 10, y);
1255                        g2.setPaint(this.subrangePaint[WARNING]);
1256                        g2.draw(line);
1257                    }
1258    
1259                    // draw start of critical range
1260                    value = this.subrangeInfo[CRITICAL][RANGE_LOW];
1261                    if (range.contains(value)) {
1262                        double x = midX + getColumnRadius() + 2;
1263                        double y = this.rangeAxis.valueToJava2D(value, dataArea, 
1264                                RectangleEdge.LEFT);
1265                        Line2D line = new Line2D.Double(x, y, x + 10, y);
1266                        g2.setPaint(this.subrangePaint[CRITICAL]);
1267                        g2.draw(line);
1268                    }
1269                }
1270    
1271                // draw the axis...
1272                if ((this.rangeAxis != null) && (this.axisLocation != NONE)) {
1273                    int drawWidth = AXIS_GAP;
1274                    if (this.showValueLines) {
1275                        drawWidth += getColumnDiameter();
1276                    }
1277                    Rectangle2D drawArea;
1278                    double cursor = 0;
1279    
1280                    switch (this.axisLocation) {
1281                        case RIGHT:
1282                            cursor = midX + getColumnRadius();
1283                            drawArea = new Rectangle2D.Double(cursor,
1284                                    stemTop, drawWidth, (stemBottom - stemTop + 1));
1285                            this.rangeAxis.draw(g2, cursor, area, drawArea, 
1286                                    RectangleEdge.RIGHT, null);
1287                            break;
1288    
1289                        case LEFT:
1290                        default:
1291                            //cursor = midX - COLUMN_RADIUS - AXIS_GAP;
1292                            cursor = midX - getColumnRadius();
1293                            drawArea = new Rectangle2D.Double(cursor, stemTop,
1294                                    drawWidth, (stemBottom - stemTop + 1));
1295                            this.rangeAxis.draw(g2, cursor, area, drawArea, 
1296                                    RectangleEdge.LEFT, null);
1297                            break;
1298                    }
1299                       
1300                }
1301    
1302                // draw text value on screen
1303                g2.setFont(this.valueFont);
1304                g2.setPaint(this.valuePaint);
1305                metrics = g2.getFontMetrics();
1306                switch (this.valueLocation) {
1307                    case RIGHT:
1308                        g2.drawString(this.valueFormat.format(current), 
1309                                midX + getColumnRadius() + getGap(), midY);
1310                        break;
1311                    case LEFT:
1312                        String valueString = this.valueFormat.format(current);
1313                        int stringWidth = metrics.stringWidth(valueString);
1314                        g2.drawString(valueString, midX - getColumnRadius() 
1315                                - getGap() - stringWidth, midY);
1316                        break;
1317                    case BULB:
1318                        temp = this.valueFormat.format(current);
1319                        i = metrics.stringWidth(temp) / 2;
1320                        g2.drawString(temp, midX - i, 
1321                                stemBottom + getBulbRadius() + getGap());
1322                        break;
1323                    default:
1324                }
1325                /***/
1326            }
1327    
1328            g2.setPaint(this.thermometerPaint);
1329            g2.setFont(this.valueFont);
1330    
1331            //  draw units indicator
1332            metrics = g2.getFontMetrics();
1333            int tickX1 = midX - getColumnRadius() - getGap() * 2
1334                         - metrics.stringWidth(UNITS[this.units]);
1335            if (tickX1 > area.getMinX()) {
1336                g2.drawString(UNITS[this.units], tickX1, 
1337                        (int) (area.getMinY() + 20));
1338            }
1339    
1340            // draw thermometer outline
1341            g2.setStroke(this.thermometerStroke);
1342            g2.draw(outerThermometer);
1343            g2.draw(innerThermometer);
1344    
1345            drawOutline(g2, area);
1346        }
1347    
1348        /**
1349         * A zoom method that does nothing.  Plots are required to support the 
1350         * zoom operation.  In the case of a thermometer chart, it doesn't make 
1351         * sense to zoom in or out, so the method is empty.
1352         *
1353         * @param percent  the zoom percentage.
1354         */
1355        public void zoom(double percent) {
1356            // intentionally blank
1357       }
1358    
1359        /**
1360         * Returns a short string describing the type of plot.
1361         *
1362         * @return A short string describing the type of plot.
1363         */
1364        public String getPlotType() {
1365            return localizationResources.getString("Thermometer_Plot");
1366        }
1367    
1368        /**
1369         * Checks to see if a new value means the axis range needs adjusting.
1370         *
1371         * @param event  the dataset change event.
1372         */
1373        public void datasetChanged(DatasetChangeEvent event) {
1374            if (this.dataset != null) {
1375                Number vn = this.dataset.getValue();
1376                if (vn != null) {
1377                    double value = vn.doubleValue();
1378                    if (inSubrange(NORMAL, value)) {
1379                        this.subrange = NORMAL;
1380                    }
1381                    else if (inSubrange(WARNING, value)) {
1382                       this.subrange = WARNING;
1383                    }
1384                    else if (inSubrange(CRITICAL, value)) {
1385                        this.subrange = CRITICAL;
1386                    }
1387                    else {
1388                        this.subrange = -1;
1389                    }
1390                    setAxisRange();
1391                }
1392            }
1393            super.datasetChanged(event);
1394        }
1395    
1396        /**
1397         * Returns the minimum value in either the domain or the range, whichever
1398         * is displayed against the vertical axis for the particular type of plot
1399         * implementing this interface.
1400         *
1401         * @return The minimum value in either the domain or the range.
1402         * 
1403         * @deprecated This method is not used.  Officially deprecated in version 
1404         *         1.0.6.
1405         */
1406        public Number getMinimumVerticalDataValue() {
1407            return new Double(this.lowerBound);
1408        }
1409    
1410        /**
1411         * Returns the maximum value in either the domain or the range, whichever
1412         * is displayed against the vertical axis for the particular type of plot
1413         * implementing this interface.
1414         *
1415         * @return The maximum value in either the domain or the range
1416         * 
1417         * @deprecated This method is not used.  Officially deprecated in version 
1418         *         1.0.6.
1419         */
1420        public Number getMaximumVerticalDataValue() {
1421            return new Double(this.upperBound);
1422        }
1423    
1424        /**
1425         * Returns the data range.
1426         *
1427         * @param axis  the axis.
1428         *
1429         * @return The range of data displayed.
1430         */
1431        public Range getDataRange(ValueAxis axis) {
1432           return new Range(this.lowerBound, this.upperBound);
1433        }
1434    
1435        /**
1436         * Sets the axis range to the current values in the rangeInfo array.
1437         */
1438        protected void setAxisRange() {
1439            if ((this.subrange >= 0) && (this.followDataInSubranges)) {
1440                this.rangeAxis.setRange(
1441                        new Range(this.subrangeInfo[this.subrange][DISPLAY_LOW],
1442                        this.subrangeInfo[this.subrange][DISPLAY_HIGH]));
1443            }
1444            else {
1445                this.rangeAxis.setRange(this.lowerBound, this.upperBound);
1446            }
1447        }
1448    
1449        /**
1450         * Returns the legend items for the plot.
1451         *
1452         * @return <code>null</code>.
1453         */
1454        public LegendItemCollection getLegendItems() {
1455            return null;
1456        }
1457    
1458        /**
1459         * Returns the orientation of the plot.
1460         * 
1461         * @return The orientation (always {@link PlotOrientation#VERTICAL}).
1462         */
1463        public PlotOrientation getOrientation() {
1464            return PlotOrientation.VERTICAL;    
1465        }
1466    
1467        /**
1468         * Determine whether a number is valid and finite.
1469         *
1470         * @param d  the number to be tested.
1471         *
1472         * @return <code>true</code> if the number is valid and finite, and 
1473         *         <code>false</code> otherwise.
1474         */
1475        protected static boolean isValidNumber(double d) {
1476            return (!(Double.isNaN(d) || Double.isInfinite(d)));
1477        }
1478    
1479        /**
1480         * Returns true if the value is in the specified range, and false otherwise.
1481         *
1482         * @param subrange  the subrange.
1483         * @param value  the value to check.
1484         *
1485         * @return A boolean.
1486         */
1487        private boolean inSubrange(int subrange, double value) {
1488            return (value > this.subrangeInfo[subrange][RANGE_LOW]
1489                && value <= this.subrangeInfo[subrange][RANGE_HIGH]);
1490        }
1491    
1492        /**
1493         * Returns the mercury paint corresponding to the current data value.
1494         * Called from the {@link #draw(Graphics2D, Rectangle2D, Point2D, 
1495         * PlotState, PlotRenderingInfo)} method.
1496         *
1497         * @return The paint (never <code>null</code>).
1498         */
1499        private Paint getCurrentPaint() {
1500            Paint result = this.mercuryPaint;
1501            if (this.useSubrangePaint) {
1502                double value = this.dataset.getValue().doubleValue();
1503                if (inSubrange(NORMAL, value)) {
1504                    result = this.subrangePaint[NORMAL];
1505                }
1506                else if (inSubrange(WARNING, value)) {
1507                    result = this.subrangePaint[WARNING];
1508                }
1509                else if (inSubrange(CRITICAL, value)) {
1510                    result = this.subrangePaint[CRITICAL];
1511                }
1512            }
1513            return result;
1514        }
1515    
1516        /**
1517         * Tests this plot for equality with another object.  The plot's dataset
1518         * is not considered in the test.
1519         *
1520         * @param obj  the object (<code>null</code> permitted).
1521         *
1522         * @return <code>true</code> or <code>false</code>.
1523         */
1524        public boolean equals(Object obj) {
1525            if (obj == this) {
1526                return true;
1527            }
1528            if (!(obj instanceof ThermometerPlot)) {
1529                return false;
1530            }
1531            ThermometerPlot that = (ThermometerPlot) obj;
1532            if (!super.equals(obj)) {
1533                return false;
1534            }
1535            if (!ObjectUtilities.equal(this.rangeAxis, that.rangeAxis)) {
1536                return false;
1537            }
1538            if (this.axisLocation != that.axisLocation) {
1539                return false;   
1540            }
1541            if (this.lowerBound != that.lowerBound) {
1542                return false;
1543            }
1544            if (this.upperBound != that.upperBound) {
1545                return false;
1546            }
1547            if (!ObjectUtilities.equal(this.padding, that.padding)) {
1548                return false;
1549            }
1550            if (!ObjectUtilities.equal(this.thermometerStroke, 
1551                    that.thermometerStroke)) {
1552                return false;
1553            }
1554            if (!PaintUtilities.equal(this.thermometerPaint, 
1555                    that.thermometerPaint)) {
1556                return false;
1557            }
1558            if (this.units != that.units) {
1559                return false;
1560            }
1561            if (this.valueLocation != that.valueLocation) {
1562                return false;
1563            }
1564            if (!ObjectUtilities.equal(this.valueFont, that.valueFont)) {
1565                return false;
1566            }
1567            if (!PaintUtilities.equal(this.valuePaint, that.valuePaint)) {
1568                return false;
1569            }
1570            if (!ObjectUtilities.equal(this.valueFormat, that.valueFormat)) {
1571                return false;
1572            }
1573            if (!PaintUtilities.equal(this.mercuryPaint, that.mercuryPaint)) {
1574                return false;
1575            }
1576            if (this.showValueLines != that.showValueLines) {
1577                return false;
1578            }
1579            if (this.subrange != that.subrange) {
1580                return false;
1581            }
1582            if (this.followDataInSubranges != that.followDataInSubranges) {
1583                return false;
1584            }
1585            if (!equal(this.subrangeInfo, that.subrangeInfo)) {
1586                return false;   
1587            }
1588            if (this.useSubrangePaint != that.useSubrangePaint) {
1589                return false;
1590            }
1591            if (this.bulbRadius != that.bulbRadius) {
1592                return false;
1593            }
1594            if (this.columnRadius != that.columnRadius) {
1595                return false;
1596            }
1597            if (this.gap != that.gap) {
1598                return false;
1599            }
1600            for (int i = 0; i < this.subrangePaint.length; i++) {
1601                if (!PaintUtilities.equal(this.subrangePaint[i], 
1602                        that.subrangePaint[i])) {
1603                    return false;   
1604                }
1605            }
1606            return true;
1607        }
1608    
1609        /**
1610         * Tests two double[][] arrays for equality.
1611         * 
1612         * @param array1  the first array (<code>null</code> permitted).
1613         * @param array2  the second arrray (<code>null</code> permitted).
1614         * 
1615         * @return A boolean.
1616         */
1617        private static boolean equal(double[][] array1, double[][] array2) {
1618            if (array1 == null) {
1619                return (array2 == null);
1620            }
1621            if (array2 == null) {
1622                return false;
1623            }
1624            if (array1.length != array2.length) {
1625                return false;
1626            }
1627            for (int i = 0; i < array1.length; i++) {
1628                if (!Arrays.equals(array1[i], array2[i])) {
1629                    return false;
1630                }
1631            }
1632            return true;
1633        }
1634    
1635        /**
1636         * Returns a clone of the plot.
1637         *
1638         * @return A clone.
1639         *
1640         * @throws CloneNotSupportedException  if the plot cannot be cloned.
1641         */
1642        public Object clone() throws CloneNotSupportedException {
1643    
1644            ThermometerPlot clone = (ThermometerPlot) super.clone();
1645    
1646            if (clone.dataset != null) {
1647                clone.dataset.addChangeListener(clone);
1648            }
1649            clone.rangeAxis = (ValueAxis) ObjectUtilities.clone(this.rangeAxis);
1650            if (clone.rangeAxis != null) {
1651                clone.rangeAxis.setPlot(clone);
1652                clone.rangeAxis.addChangeListener(clone);
1653            }
1654            clone.valueFormat = (NumberFormat) this.valueFormat.clone();
1655            clone.subrangePaint = (Paint[]) this.subrangePaint.clone();
1656    
1657            return clone;
1658    
1659        }
1660    
1661        /**
1662         * Provides serialization support.
1663         *
1664         * @param stream  the output stream.
1665         *
1666         * @throws IOException  if there is an I/O error.
1667         */
1668        private void writeObject(ObjectOutputStream stream) throws IOException { 
1669            stream.defaultWriteObject();
1670            SerialUtilities.writeStroke(this.thermometerStroke, stream);
1671            SerialUtilities.writePaint(this.thermometerPaint, stream);
1672            SerialUtilities.writePaint(this.valuePaint, stream);
1673            SerialUtilities.writePaint(this.mercuryPaint, stream);
1674            SerialUtilities.writeStroke(this.subrangeIndicatorStroke, stream);
1675            SerialUtilities.writeStroke(this.rangeIndicatorStroke, stream);
1676            for (int i = 0; i < 3; i++) {
1677                SerialUtilities.writePaint(this.subrangePaint[i], stream);
1678            }
1679        }
1680    
1681        /**
1682         * Provides serialization support.
1683         *
1684         * @param stream  the input stream.
1685         *
1686         * @throws IOException  if there is an I/O error.
1687         * @throws ClassNotFoundException  if there is a classpath problem.
1688         */
1689        private void readObject(ObjectInputStream stream) throws IOException,
1690                ClassNotFoundException {
1691            stream.defaultReadObject();
1692            this.thermometerStroke = SerialUtilities.readStroke(stream);
1693            this.thermometerPaint = SerialUtilities.readPaint(stream);
1694            this.valuePaint = SerialUtilities.readPaint(stream);
1695            this.mercuryPaint = SerialUtilities.readPaint(stream);
1696            this.subrangeIndicatorStroke = SerialUtilities.readStroke(stream);
1697            this.rangeIndicatorStroke = SerialUtilities.readStroke(stream);
1698            this.subrangePaint = new Paint[3];
1699            for (int i = 0; i < 3; i++) {
1700                this.subrangePaint[i] = SerialUtilities.readPaint(stream);
1701            }
1702            if (this.rangeAxis != null) {
1703                this.rangeAxis.addChangeListener(this);
1704            }
1705        }
1706    
1707        /**
1708         * Multiplies the range on the domain axis/axes by the specified factor.
1709         *
1710         * @param factor  the zoom factor.
1711         * @param state  the plot state.
1712         * @param source  the source point.
1713         */
1714        public void zoomDomainAxes(double factor, PlotRenderingInfo state, 
1715                                   Point2D source) {
1716            // no domain axis to zoom
1717        }
1718    
1719        /**
1720         * Multiplies the range on the domain axis/axes by the specified factor.
1721         *
1722         * @param factor  the zoom factor.
1723         * @param state  the plot state.
1724         * @param source  the source point.
1725         * @param useAnchor  a flag that controls whether or not the source point
1726         *         is used for the zoom anchor.
1727         *         
1728         * @since 1.0.7
1729         */
1730        public void zoomDomainAxes(double factor, PlotRenderingInfo state, 
1731                                   Point2D source, boolean useAnchor) {
1732            // no domain axis to zoom
1733        }
1734        
1735        /**
1736         * Multiplies the range on the range axis/axes by the specified factor.
1737         *
1738         * @param factor  the zoom factor.
1739         * @param state  the plot state.
1740         * @param source  the source point.
1741         */
1742        public void zoomRangeAxes(double factor, PlotRenderingInfo state, 
1743                                  Point2D source) {
1744            this.rangeAxis.resizeRange(factor);
1745        }
1746    
1747        /**
1748         * Multiplies the range on the range axis/axes by the specified factor.
1749         *
1750         * @param factor  the zoom factor.
1751         * @param state  the plot state.
1752         * @param source  the source point.
1753         * @param useAnchor  a flag that controls whether or not the source point
1754         *         is used for the zoom anchor.
1755         *         
1756         * @since 1.0.7
1757         */
1758        public void zoomRangeAxes(double factor, PlotRenderingInfo state, 
1759                                  Point2D source, boolean useAnchor) {
1760            double anchorY = this.getRangeAxis().java2DToValue(source.getY(), 
1761                    state.getDataArea(), RectangleEdge.LEFT);
1762            this.rangeAxis.resizeRange(factor, anchorY);
1763        }
1764        
1765        /**
1766         * This method does nothing.
1767         *
1768         * @param lowerPercent  the lower percent.
1769         * @param upperPercent  the upper percent.
1770         * @param state  the plot state.
1771         * @param source  the source point.
1772         */
1773        public void zoomDomainAxes(double lowerPercent, double upperPercent, 
1774                                   PlotRenderingInfo state, Point2D source) {
1775            // no domain axis to zoom
1776        }
1777    
1778        /**
1779         * Zooms the range axes.
1780         *
1781         * @param lowerPercent  the lower percent.
1782         * @param upperPercent  the upper percent.
1783         * @param state  the plot state.
1784         * @param source  the source point.
1785         */
1786        public void zoomRangeAxes(double lowerPercent, double upperPercent, 
1787                                  PlotRenderingInfo state, Point2D source) {
1788            this.rangeAxis.zoomRange(lowerPercent, upperPercent);
1789        }
1790      
1791        /**
1792         * Returns <code>false</code>.
1793         * 
1794         * @return A boolean.
1795         */
1796        public boolean isDomainZoomable() {
1797            return false;
1798        }
1799        
1800        /**
1801         * Returns <code>true</code>.
1802         * 
1803         * @return A boolean.
1804         */
1805        public boolean isRangeZoomable() {
1806            return true;
1807        }
1808    
1809    }