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