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
093package org.jfree.chart.plot;
094
095import java.awt.BasicStroke;
096import java.awt.Color;
097import java.awt.Font;
098import java.awt.FontMetrics;
099import java.awt.Graphics2D;
100import java.awt.Paint;
101import java.awt.Stroke;
102import java.awt.geom.Area;
103import java.awt.geom.Ellipse2D;
104import java.awt.geom.Line2D;
105import java.awt.geom.Point2D;
106import java.awt.geom.Rectangle2D;
107import java.awt.geom.RoundRectangle2D;
108import java.io.IOException;
109import java.io.ObjectInputStream;
110import java.io.ObjectOutputStream;
111import java.io.Serializable;
112import java.text.DecimalFormat;
113import java.text.NumberFormat;
114import java.util.Arrays;
115import java.util.ResourceBundle;
116
117import org.jfree.chart.LegendItemCollection;
118import org.jfree.chart.axis.NumberAxis;
119import org.jfree.chart.axis.ValueAxis;
120import org.jfree.chart.event.PlotChangeEvent;
121import org.jfree.chart.util.ResourceBundleWrapper;
122import org.jfree.data.Range;
123import org.jfree.data.general.DatasetChangeEvent;
124import org.jfree.data.general.DefaultValueDataset;
125import org.jfree.data.general.ValueDataset;
126import org.jfree.io.SerialUtilities;
127import org.jfree.ui.RectangleEdge;
128import org.jfree.ui.RectangleInsets;
129import org.jfree.util.ObjectUtilities;
130import org.jfree.util.PaintUtilities;
131import 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 */
151public 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}