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