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