    001    /* ===========================================================
    002     * JFreeChart : a free chart library for the Java(tm) platform
    003     * ===========================================================
    004     *
    005     * (C) Copyright 2000-2007, by Object Refinery Limited and Contributors.
    006     *
    007     * Project Info:  http://www.jfree.org/jfreechart/index.html
    008     *
    009     * This library is free software; you can redistribute it and/or modify it
    010     * under the terms of the GNU Lesser General Public License as published by
    011     * the Free Software Foundation; either version 2.1 of the License, or
    012     * (at your option) any later version.
    013     *
    014     * This library is distributed in the hope that it will be useful, but
    015     * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
    016     * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
    017     * License for more details.
    018     *
    019     * You should have received a copy of the GNU Lesser General Public
    020     * License along with this library; if not, write to the Free Software
    021     * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301,
    022     * USA.
    023     *
    024     * [Java is a trademark or registered trademark of Sun Microsystems, Inc.
    025     * in the United States and other countries.]
    026     *
    027     * --------------------
    028     * ThermometerPlot.java
    029     * --------------------
    030     *
    031     * (C) Copyright 2000-2007, by Bryan Scott and Contributors.
    032     *
    033     * Original Author:  Bryan Scott (based on MeterPlot by Hari).
    034     * Contributor(s):   David Gilbert (for Object Refinery Limited).
    035     *                   Arnaud Lelievre;
    036     *
    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     * ------------- JFREECHART 1.0.x ---------------------------------------------
    076     * 14-Nov-2006 : Fixed margin when drawing (DG);
    077     *
    078     */
    079
    080    package org.jfree.chart.plot;
    081
    082    import java.awt.BasicStroke;
    083    import java.awt.Color;
    084    import java.awt.Font;
    085    import java.awt.FontMetrics;
    086    import java.awt.Graphics2D;
    087    import java.awt.Paint;
    088    import java.awt.Stroke;
    089    import java.awt.geom.Area;
    090    import java.awt.geom.Ellipse2D;
    091    import java.awt.geom.Line2D;
    092    import java.awt.geom.Point2D;
    093    import java.awt.geom.Rectangle2D;
    094    import java.awt.geom.RoundRectangle2D;
    095    import java.io.IOException;
    096    import java.io.ObjectInputStream;
    097    import java.io.ObjectOutputStream;
    098    import java.io.Serializable;
    099    import java.text.DecimalFormat;
    100    import java.text.NumberFormat;
    101    import java.util.Arrays;
    102    import java.util.ResourceBundle;
    103
    104    import org.jfree.chart.LegendItemCollection;
    105    import org.jfree.chart.axis.NumberAxis;
    106    import org.jfree.chart.axis.ValueAxis;
    107    import org.jfree.chart.event.PlotChangeEvent;
    108    import org.jfree.data.Range;
    109    import org.jfree.data.general.DatasetChangeEvent;
    110    import org.jfree.data.general.DefaultValueDataset;
    111    import org.jfree.data.general.ValueDataset;
    112    import org.jfree.io.SerialUtilities;
    113    import org.jfree.ui.RectangleEdge;
    114    import org.jfree.ui.RectangleInsets;
    115    import org.jfree.util.ObjectUtilities;
    116    import org.jfree.util.PaintUtilities;
    117    import org.jfree.util.UnitType;
    118
    119    /**
    120     * A plot that displays a single value (from a {@link ValueDataset}) in a
    121     * thermometer type display.
    122     * <p>
    123     * This plot supports a number of options:
    124     * <ol>
    125     * <li>three sub-ranges which could be viewed as 'Normal', 'Warning'
    126     *   and 'Critical' ranges.</li>
    127     * <li>the thermometer can be run in two modes:
    128     *      <ul>
    129     *      <li>fixed range, or</li>
    130     *      <li>range adjusts to current sub-range.</li>
    131     *      </ul>
    132     * </li>
    133     * <li>settable units to be displayed.</li>
    134     * <li>settable display location for the value text.</li>
    135     * </ol>
    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(UnitType.RELATIVE, 0.05, 0.05, 0.05,
    327                    0.05);
    328            this.dataset = dataset;
    329            if (dataset != null) {
    330                dataset.addChangeListener(this);
    331            }
    332            NumberAxis axis = new NumberAxis(null);
    333            axis.setStandardTickUnits(NumberAxis.createIntegerTickUnits());
    334            axis.setAxisLineVisible(false);
    335
    336            setRangeAxis(axis);
    337            setAxisRange();
    338        }
    339
    340        /**
    341         * Returns the primary dataset for the plot.
    342         *
    343         * @return The primary dataset (possibly <code>null</code>).
    344         */
    345        public ValueDataset getDataset() {
    346            return this.dataset;
    347        }
    348
    349        /**
    350         * Sets the dataset for the plot, replacing the existing dataset if there
    351         * is one.
    352         *
    353         * @param dataset  the dataset (<code>null</code> permitted).
    354         */
    355        public void setDataset(ValueDataset dataset) {
    356
    357            // if there is an existing dataset, remove the plot from the list
    358            // of change listeners...
    359            ValueDataset existing = this.dataset;
    360            if (existing != null) {
    361                existing.removeChangeListener(this);
    362            }
    363
    364            // set the new dataset, and register the chart as a change listener...
    365            this.dataset = dataset;
    366            if (dataset != null) {
    367                setDatasetGroup(dataset.getGroup());
    368                dataset.addChangeListener(this);
    369            }
    370
    371            // send a dataset change event to self...
    372            DatasetChangeEvent event = new DatasetChangeEvent(this, dataset);
    373            datasetChanged(event);
    374
    375        }
    376
    377        /**
    378         * Returns the range axis.
    379         *
    380         * @return The range axis.
    381         */
    382        public ValueAxis getRangeAxis() {
    383            return this.rangeAxis;
    384        }
    385
    386        /**
    387         * Sets the range axis for the plot.
    388         *
    389         * @param axis  the new axis.
    390         */
    391        public void setRangeAxis(ValueAxis axis) {
    392
    393            if (axis != null) {
    394                axis.setPlot(this);
    395                axis.addChangeListener(this);
    396            }
    397
    398            // plot is likely registered as a listener with the existing axis...
    399            if (this.rangeAxis != null) {
    400                this.rangeAxis.removeChangeListener(this);
    401            }
    402
    403            this.rangeAxis = axis;
    404
    405        }
    406
    407        /**
    408         * Returns the lower bound for the thermometer.  The data value can be set
    409         * lower than this, but it will not be shown in the thermometer.
    410         *
    411         * @return The lower bound.
    412         *
    413         */
    414        public double getLowerBound() {
    415            return this.lowerBound;
    416        }
    417
    418        /**
    419         * Sets the lower bound for the thermometer.
    420         *
    421         * @param lower the lower bound.
    422         */
    423        public void setLowerBound(double lower) {
    424            this.lowerBound = lower;
    425            setAxisRange();
    426        }
    427
    428        /**
    429         * Returns the upper bound for the thermometer.  The data value can be set
    430         * higher than this, but it will not be shown in the thermometer.
    431         *
    432         * @return The upper bound.
    433         */
    434        public double getUpperBound() {
    435            return this.upperBound;
    436        }
    437
    438        /**
    439         * Sets the upper bound for the thermometer.
    440         *
    441         * @param upper the upper bound.
    442         */
    443        public void setUpperBound(double upper) {
    444            this.upperBound = upper;
    445            setAxisRange();
    446        }
    447
    448        /**
    449         * Sets the lower and upper bounds for the thermometer.
    450         *
    451         * @param lower  the lower bound.
    452         * @param upper  the upper bound.
    453         */
    454        public void setRange(double lower, double upper) {
    455            this.lowerBound = lower;
    456            this.upperBound = upper;
    457            setAxisRange();
    458        }
    459
    460        /**
    461         * Returns the padding for the thermometer.  This is the space inside the
    462         * plot area.
    463         *
    464         * @return The padding.
    465         */
    466        public RectangleInsets getPadding() {
    467            return this.padding;
    468        }
    469
    470        /**
    471         * Sets the padding for the thermometer.
    472         *
    473         * @param padding  the padding.
    474         */
    475        public void setPadding(RectangleInsets padding) {
    476            this.padding = padding;
    477            notifyListeners(new PlotChangeEvent(this));
    478        }
    479
    480        /**
    481         * Returns the stroke used to draw the thermometer outline.
    482         *
    483         * @return The stroke.
    484         */
    485        public Stroke getThermometerStroke() {
    486            return this.thermometerStroke;
    487        }
    488
    489        /**
    490         * Sets the stroke used to draw the thermometer outline.
    491         *
    492         * @param s  the new stroke (null ignored).
    493         */
    494        public void setThermometerStroke(Stroke s) {
    495            if (s != null) {
    496                this.thermometerStroke = s;
    497                notifyListeners(new PlotChangeEvent(this));
    498            }
    499        }
    500
    501        /**
    502         * Returns the paint used to draw the thermometer outline.
    503         *
    504         * @return The paint.
    505         */
    506        public Paint getThermometerPaint() {
    507            return this.thermometerPaint;
    508        }
    509
    510        /**
    511         * Sets the paint used to draw the thermometer outline.
    512         *
    513         * @param paint  the new paint (null ignored).
    514         */
    515        public void setThermometerPaint(Paint paint) {
    516            if (paint != null) {
    517                this.thermometerPaint = paint;
    518                notifyListeners(new PlotChangeEvent(this));
    519            }
    520        }
    521
    522        /**
    523         * Returns the unit display type (none/Fahrenheit/Celcius/Kelvin).
    524         *
    525         * @return The units type.
    526         */
    527        public int getUnits() {
    528            return this.units;
    529        }
    530
    531        /**
    532         * Sets the units to be displayed in the thermometer.
    533         * <p>
    534         * Use one of the following constants:
    535         *
    536         * <ul>
    537         * <li>UNITS_NONE : no units displayed.</li>
    538         * <li>UNITS_FAHRENHEIT : units displayed in Fahrenheit.</li>
    539         * <li>UNITS_CELCIUS : units displayed in Celcius.</li>
    540         * <li>UNITS_KELVIN : units displayed in Kelvin.</li>
    541         * </ul>
    542         *
    543         * @param u  the new unit type.
    544         */
    545        public void setUnits(int u) {
    546            if ((u >= 0) && (u < UNITS.length)) {
    547                if (this.units != u) {
    548                    this.units = u;
    549                    notifyListeners(new PlotChangeEvent(this));
    550                }
    551            }
    552        }
    553
    554        /**
    555         * Sets the unit type.
    556         *
    557         * @param u  the unit type (null ignored).
    558         */
    559        public void setUnits(String u) {
    560            if (u == null) {
    561                return;
    562            }
    563
    564            u = u.toUpperCase().trim();
    565            for (int i = 0; i < UNITS.length; ++i) {
    566                if (u.equals(UNITS[i].toUpperCase().trim())) {
    567                    setUnits(i);
    568                    i = UNITS.length;
    569                }
    570            }
    571        }
    572
    573        /**
    574         * Returns the value location.
    575         *
    576         * @return The location.
    577         */
    578        public int getValueLocation() {
    579            return this.valueLocation;
    580        }
    581
    582        /**
    583         * Sets the location at which the current value is displayed.
    584         * <P>
    585         * The location can be one of the constants:
    586         * <code>NONE</code>,
    587         * <code>RIGHT</code>
    588         * <code>LEFT</code> and
    589         * <code>BULB</code>.
    590         *
    591         * @param location  the location.
    592         */
    593        public void setValueLocation(int location) {
    594            if ((location >= 0) && (location < 4)) {
    595                this.valueLocation = location;
    596                notifyListeners(new PlotChangeEvent(this));
    597            }
    598            else {
    599                throw new IllegalArgumentException("Location not recognised.");
    600            }
    601        }
    602
    603        /**
    604         * Sets the location at which the axis is displayed with reference to the
    605         * bulb.
    606         * <P>
    607         * The location can be one of the constants:
    608         *   <code>NONE</code>,
    609         *   <code>RIGHT</code> and
    610         *   <code>LEFT</code>.
    611         *
    612         * @param location  the location.
    613         */
    614        public void setAxisLocation(int location) {
    615            if ((location >= 0) && (location < 3)) {
    616                this.axisLocation = location;
    617                notifyListeners(new PlotChangeEvent(this));
    618            }
    619            else {
    620                throw new IllegalArgumentException("Location not recognised.");
    621            }
    622        }
    623
    624        /**
    625         * Returns the axis location.
    626         *
    627         * @return The location.
    628         */
    629        public int getAxisLocation() {
    630            return this.axisLocation;
    631        }
    632
    633        /**
    634         * Gets the font used to display the current value.
    635         *
    636         * @return The font.
    637         */
    638        public Font getValueFont() {
    639            return this.valueFont;
    640        }
    641
    642        /**
    643         * Sets the font used to display the current value.
    644         *
    645         * @param f  the new font.
    646         */
    647        public void setValueFont(Font f) {
    648            if ((f != null) && (!this.valueFont.equals(f))) {
    649                this.valueFont = f;
    650                notifyListeners(new PlotChangeEvent(this));
    651            }
    652        }
    653
    654        /**
    655         * Gets the paint used to display the current value.
    656        *
    657         * @return The paint.
    658         */
    659        public Paint getValuePaint() {
    660            return this.valuePaint;
    661        }
    662
    663        /**
    664         * Sets the paint used to display the current value.
    665         *
    666         * @param p  the new paint.
    667         */
    668        public void setValuePaint(Paint p) {
    669            if ((p != null) && (!this.valuePaint.equals(p))) {
    670                this.valuePaint = p;
    671                notifyListeners(new PlotChangeEvent(this));
    672            }
    673        }
    674
    675        /**
    676         * Sets the formatter for the value label.
    677         *
    678         * @param formatter  the new formatter.
    679         */
    680        public void setValueFormat(NumberFormat formatter) {
    681            if (formatter != null) {
    682                this.valueFormat = formatter;
    683                notifyListeners(new PlotChangeEvent(this));
    684            }
    685        }
    686
    687        /**
    688         * Returns the default mercury paint.
    689         *
    690         * @return The paint.
    691         */
    692        public Paint getMercuryPaint() {
    693            return this.mercuryPaint;
    694        }
    695
    696        /**
    697         * Sets the default mercury paint.
    698         *
    699         * @param paint  the new paint.
    700         */
    701        public void setMercuryPaint(Paint paint) {
    702            this.mercuryPaint = paint;
    703            notifyListeners(new PlotChangeEvent(this));
    704        }
    705
    706        /**
    707         * Returns the flag that controls whether not value lines are displayed.
    708         *
    709         * @return The flag.
    710         */
    711        public boolean getShowValueLines() {
    712            return this.showValueLines;
    713        }
    714
    715        /**
    716         * Sets the display as to whether to show value lines in the output.
    717         *
    718         * @param b Whether to show value lines in the thermometer
    719         */
    720        public void setShowValueLines(boolean b) {
    721            this.showValueLines = b;
    722            notifyListeners(new PlotChangeEvent(this));
    723        }
    724
    725        /**
    726         * Sets information for a particular range.
    727         *
    728         * @param range  the range to specify information about.
    729         * @param low  the low value for the range
    730         * @param hi  the high value for the range
    731         */
    732        public void setSubrangeInfo(int range, double low, double hi) {
    733            setSubrangeInfo(range, low, hi, low, hi);
    734        }
    735
    736        /**
    737         * Sets the subrangeInfo attribute of the ThermometerPlot object
    738         *
    739         * @param range  the new rangeInfo value.
    740         * @param rangeLow  the new rangeInfo value
    741         * @param rangeHigh  the new rangeInfo value
    742         * @param displayLow  the new rangeInfo value
    743         * @param displayHigh  the new rangeInfo value
    744         */
    745        public void setSubrangeInfo(int range,
    746                                    double rangeLow, double rangeHigh,
    747                                    double displayLow, double displayHigh) {
    748
    749            if ((range >= 0) && (range < 3)) {
    750                setSubrange(range, rangeLow, rangeHigh);
    751                setDisplayRange(range, displayLow, displayHigh);
    752                setAxisRange();
    753                notifyListeners(new PlotChangeEvent(this));
    754            }
    755
    756        }
    757
    758        /**
    759         * Sets the range.
    760         *
    761         * @param range  the range type.
    762         * @param low  the low value.
    763         * @param high  the high value.
    764         */
    765        public void setSubrange(int range, double low, double high) {
    766            if ((range >= 0) && (range < 3)) {
    767                this.subrangeInfo[range][RANGE_HIGH] = high;
    768                this.subrangeInfo[range][RANGE_LOW] = low;
    769            }
    770        }
    771
    772        /**
    773         * Sets the display range.
    774         *
    775         * @param range  the range type.
    776         * @param low  the low value.
    777         * @param high  the high value.
    778         */
    779        public void setDisplayRange(int range, double low, double high) {
    780
    781            if ((range >= 0) && (range < this.subrangeInfo.length)
    782                && isValidNumber(high) && isValidNumber(low)) {
    783
    784                if (high > low) {
    785                    this.subrangeInfo[range][DISPLAY_HIGH] = high;
    786                    this.subrangeInfo[range][DISPLAY_LOW] = low;
    787                }
    788                else {
    789                    this.subrangeInfo[range][DISPLAY_HIGH] = low;
    790                    this.subrangeInfo[range][DISPLAY_LOW] = high;
    791                }
    792
    793            }
    794
    795        }
    796
    797        /**
    798         * Gets the paint used for a particular subrange.
    799         *
    800         * @param range  the range.
    801         *
    802         * @return The paint.
    803         */
    804        public Paint getSubrangePaint(int range) {
    805            if ((range >= 0) && (range < this.subrangePaint.length)) {
    806                return this.subrangePaint[range];
    807            }
    808            else {
    809                return this.mercuryPaint;
    810            }
    811        }
    812
    813        /**
    814         * Sets the paint to be used for a range.
    815         *
    816         * @param range  the range.
    817         * @param paint  the paint to be applied.
    818         */
    819        public void setSubrangePaint(int range, Paint paint) {
    820            if ((range >= 0)
    821                    && (range < this.subrangePaint.length) && (paint != null)) {
    822                this.subrangePaint[range] = paint;
    823                notifyListeners(new PlotChangeEvent(this));
    824            }
    825        }
    826
    827        /**
    828         * Returns a flag that controls whether or not the thermometer axis zooms
    829         * to display the subrange within which the data value falls.
    830         *
    831         * @return The flag.
    832         */
    833        public boolean getFollowDataInSubranges() {
    834            return this.followDataInSubranges;
    835        }
    836
    837        /**
    838         * Sets the flag that controls whether or not the thermometer axis zooms
    839         * to display the subrange within which the data value falls.
    840         *
    841         * @param flag  the flag.
    842         */
    843        public void setFollowDataInSubranges(boolean flag) {
    844            this.followDataInSubranges = flag;
    845            notifyListeners(new PlotChangeEvent(this));
    846        }
    847
    848        /**
    849         * Returns a flag that controls whether or not the mercury color changes
    850         * for each subrange.
    851         *
    852         * @return The flag.
    853         */
    854        public boolean getUseSubrangePaint() {
    855            return this.useSubrangePaint;
    856        }
    857
    858        /**
    859         * Sets the range colour change option.
    860         *
    861         * @param flag The new range colour change option
    862         */
    863        public void setUseSubrangePaint(boolean flag) {
    864            this.useSubrangePaint = flag;
    865            notifyListeners(new PlotChangeEvent(this));
    866        }
    867
    868        /**
    869         * Draws the plot on a Java 2D graphics device (such as the screen or a
    870         * printer).
    871         *
    872         * @param g2  the graphics device.
    873         * @param area  the area within which the plot should be drawn.
    874         * @param anchor  the anchor point (<code>null</code> permitted).
    875         * @param parentState  the state from the parent plot, if there is one.
    876         * @param info  collects info about the drawing.
    877         */
    878        public void draw(Graphics2D g2, Rectangle2D area, Point2D anchor,
    879                         PlotState parentState,
    880                         PlotRenderingInfo info) {
    881
    882            RoundRectangle2D outerStem = new RoundRectangle2D.Double();
    883            RoundRectangle2D innerStem = new RoundRectangle2D.Double();
    884            RoundRectangle2D mercuryStem = new RoundRectangle2D.Double();
    885            Ellipse2D outerBulb = new Ellipse2D.Double();
    886            Ellipse2D innerBulb = new Ellipse2D.Double();
    887            String temp = null;
    888            FontMetrics metrics = null;
    889            if (info != null) {
    890                info.setPlotArea(area);
    891            }
    892
    893            // adjust for insets...
    894            RectangleInsets insets = getInsets();
    895            insets.trim(area);
    896            drawBackground(g2, area);
    897
    898            // adjust for padding...
    899            Rectangle2D interior = (Rectangle2D) area.clone();
    900            this.padding.trim(interior);
    901            int midX = (int) (interior.getX() + (interior.getWidth() / 2));
    902            int midY = (int) (interior.getY() + (interior.getHeight() / 2));
    903            int stemTop = (int) (interior.getMinY() + BULB_RADIUS);
    904            int stemBottom = (int) (interior.getMaxY() - BULB_DIAMETER);
    905            Rectangle2D dataArea = new Rectangle2D.Double(midX - COLUMN_RADIUS,
    906                    stemTop, COLUMN_RADIUS, stemBottom - stemTop);
    907
    908            outerBulb.setFrame(midX - BULB_RADIUS, stemBottom, BULB_DIAMETER,
    909                    BULB_DIAMETER);
    910
    911            outerStem.setRoundRect(midX - COLUMN_RADIUS, interior.getMinY(),
    912                    COLUMN_DIAMETER, stemBottom + BULB_DIAMETER - stemTop,
    913                    COLUMN_DIAMETER, COLUMN_DIAMETER);
    914
    915            Area outerThermometer = new Area(outerBulb);
    916            Area tempArea = new Area(outerStem);
    917            outerThermometer.add(tempArea);
    918
    919            innerBulb.setFrame(midX - BULB_RADIUS + GAP_RADIUS,
    920                    stemBottom + GAP_RADIUS, BULB_DIAMETER - GAP_DIAMETER,
    921                    BULB_DIAMETER - GAP_DIAMETER);
    922
    923            innerStem.setRoundRect(midX - COLUMN_RADIUS + GAP_RADIUS,
    924                    interior.getMinY() + GAP_RADIUS, COLUMN_DIAMETER - GAP_DIAMETER,
    925                    stemBottom + BULB_DIAMETER - GAP_DIAMETER - stemTop,
    926                    COLUMN_DIAMETER - GAP_DIAMETER, COLUMN_DIAMETER - GAP_DIAMETER);
    927
    928            Area innerThermometer = new Area(innerBulb);
    929            tempArea = new Area(innerStem);
    930            innerThermometer.add(tempArea);
    931
    932            if ((this.dataset != null) && (this.dataset.getValue() != null)) {
    933                double current = this.dataset.getValue().doubleValue();
    934                double ds = this.rangeAxis.valueToJava2D(current, dataArea,
    935                        RectangleEdge.LEFT);
    936
    937                int i = COLUMN_DIAMETER - GAP_DIAMETER; // already calculated
    938                int j = COLUMN_RADIUS - GAP_RADIUS; // already calculated
    939                int l = (i / 2);
    940                int k = (int) Math.round(ds);
    941                if (k < (GAP_RADIUS + interior.getMinY())) {
    942                    k = (int) (GAP_RADIUS + interior.getMinY());
    943                    l = BULB_RADIUS;
    944                }
    945
    946                Area mercury = new Area(innerBulb);
    947
    948                if (k < (stemBottom + BULB_RADIUS)) {
    949                    mercuryStem.setRoundRect(midX - j, k, i,
    950                            (stemBottom + BULB_RADIUS) - k, l, l);
    951                    tempArea = new Area(mercuryStem);
    952                    mercury.add(tempArea);
    953                }
    954
    955                g2.setPaint(getCurrentPaint());
    956                g2.fill(mercury);
    957
    958                // draw range indicators...
    959                if (this.subrangeIndicatorsVisible) {
    960                    g2.setStroke(this.subrangeIndicatorStroke);
    961                    Range range = this.rangeAxis.getRange();
    962
    963                    // draw start of normal range
    964                    double value = this.subrangeInfo[NORMAL][RANGE_LOW];
    965                    if (range.contains(value)) {
    966                        double x = midX + COLUMN_RADIUS + 2;
    967                        double y = this.rangeAxis.valueToJava2D(value, dataArea,
    968                                RectangleEdge.LEFT);
    969                        Line2D line = new Line2D.Double(x, y, x + 10, y);
    970                        g2.setPaint(this.subrangePaint[NORMAL]);
    971                        g2.draw(line);
    972                    }
    973
    974                    // draw start of warning range
    975                    value = this.subrangeInfo[WARNING][RANGE_LOW];
    976                    if (range.contains(value)) {
    977                        double x = midX + COLUMN_RADIUS + 2;
    978                        double y = this.rangeAxis.valueToJava2D(value, dataArea,
    979                                RectangleEdge.LEFT);
    980                        Line2D line = new Line2D.Double(x, y, x + 10, y);
    981                        g2.setPaint(this.subrangePaint[WARNING]);
    982                        g2.draw(line);
    983                    }
    984
    985                    // draw start of critical range
    986                    value = this.subrangeInfo[CRITICAL][RANGE_LOW];
    987                    if (range.contains(value)) {
    988                        double x = midX + COLUMN_RADIUS + 2;
    989                        double y = this.rangeAxis.valueToJava2D(value, dataArea,
    990                                RectangleEdge.LEFT);
    991                        Line2D line = new Line2D.Double(x, y, x + 10, y);
    992                        g2.setPaint(this.subrangePaint[CRITICAL]);
    993                        g2.draw(line);
    994                    }
    995                }
    996
    997                // draw the axis...
    998                if ((this.rangeAxis != null) && (this.axisLocation != NONE)) {
    999                    int drawWidth = AXIS_GAP;
    1000                    if (this.showValueLines) {
    1001                        drawWidth += COLUMN_DIAMETER;
    1002                    }
    1003                    Rectangle2D drawArea;
    1004                    double cursor = 0;
    1005
    1006                    switch (this.axisLocation) {
    1007                        case RIGHT:
    1008                            cursor = midX + COLUMN_RADIUS;
    1009                            drawArea = new Rectangle2D.Double(cursor,
    1010                                    stemTop, drawWidth, (stemBottom - stemTop + 1));
    1011                            this.rangeAxis.draw(g2, cursor, area, drawArea,
    1012                                    RectangleEdge.RIGHT, null);
    1013                            break;
    1014
    1015                        case LEFT:
    1016                        default:
    1017                            //cursor = midX - COLUMN_RADIUS - AXIS_GAP;
    1018                            cursor = midX - COLUMN_RADIUS;
    1019                            drawArea = new Rectangle2D.Double(cursor, stemTop,
    1020                                    drawWidth, (stemBottom - stemTop + 1));
    1021                            this.rangeAxis.draw(g2, cursor, area, drawArea,
    1022                                    RectangleEdge.LEFT, null);
    1023                            break;
    1024                    }
    1025
    1026                }
    1027
    1028                // draw text value on screen
    1029                g2.setFont(this.valueFont);
    1030                g2.setPaint(this.valuePaint);
    1031                metrics = g2.getFontMetrics();
    1032                switch (this.valueLocation) {
    1033                    case RIGHT:
    1034                        g2.drawString(this.valueFormat.format(current),
    1035                                midX + COLUMN_RADIUS + GAP_RADIUS, midY);
    1036                        break;
    1037                    case LEFT:
    1038                        String valueString = this.valueFormat.format(current);
    1039                        int stringWidth = metrics.stringWidth(valueString);
    1040                        g2.drawString(valueString, midX - COLUMN_RADIUS
    1041                                - GAP_RADIUS - stringWidth, midY);
    1042                        break;
    1043                    case BULB:
    1044                        temp = this.valueFormat.format(current);
    1045                        i = metrics.stringWidth(temp) / 2;
    1046                        g2.drawString(temp, midX - i,
    1047                                stemBottom + BULB_RADIUS + GAP_RADIUS);
    1048                        break;
    1049                    default:
    1050                }
    1051                /***/
    1052            }
    1053
    1054            g2.setPaint(this.thermometerPaint);
    1055            g2.setFont(this.valueFont);
    1056
    1057            //  draw units indicator
    1058            metrics = g2.getFontMetrics();
    1059            int tickX1 = midX - COLUMN_RADIUS - GAP_DIAMETER
    1060                         - metrics.stringWidth(UNITS[this.units]);
    1061            if (tickX1 > area.getMinX()) {
    1062                g2.drawString(UNITS[this.units], tickX1,
    1063                        (int) (area.getMinY() + 20));
    1064            }
    1065
    1066            // draw thermometer outline
    1067            g2.setStroke(this.thermometerStroke);
    1068            g2.draw(outerThermometer);
    1069            g2.draw(innerThermometer);
    1070
    1071            drawOutline(g2, area);
    1072        }
    1073
    1074        /**
    1075         * A zoom method that does nothing.  Plots are required to support the
    1076         * zoom operation.  In the case of a thermometer chart, it doesn't make
    1077         * sense to zoom in or out, so the method is empty.
    1078         *
    1079         * @param percent  the zoom percentage.
    1080         */
    1081        public void zoom(double percent) {
    1082            // intentionally blank
    1083       }
    1084
    1085        /**
    1086         * Returns a short string describing the type of plot.
    1087         *
    1088         * @return A short string describing the type of plot.
    1089         */
    1090        public String getPlotType() {
    1091            return localizationResources.getString("Thermometer_Plot");
    1092        }
    1093
    1094        /**
    1095         * Checks to see if a new value means the axis range needs adjusting.
    1096         *
    1097         * @param event  the dataset change event.
    1098         */
    1099        public void datasetChanged(DatasetChangeEvent event) {
    1100            Number vn = this.dataset.getValue();
    1101            if (vn != null) {
    1102                double value = vn.doubleValue();
    1103                if (inSubrange(NORMAL, value)) {
    1104                    this.subrange = NORMAL;
    1105                }
    1106                else if (inSubrange(WARNING, value)) {
    1107                   this.subrange = WARNING;
    1108                }
    1109                else if (inSubrange(CRITICAL, value)) {
    1110                    this.subrange = CRITICAL;
    1111                }
    1112                else {
    1113                    this.subrange = -1;
    1114                }
    1115                setAxisRange();
    1116            }
    1117            super.datasetChanged(event);
    1118        }
    1119
    1120        /**
    1121         * Returns the minimum value in either the domain or the range, whichever
    1122         * is displayed against the vertical axis for the particular type of plot
    1123         * implementing this interface.
    1124         *
    1125         * @return The minimum value in either the domain or the range.
    1126         */
    1127        public Number getMinimumVerticalDataValue() {
    1128            return new Double(this.lowerBound);
    1129        }
    1130
    1131        /**
    1132         * Returns the maximum value in either the domain or the range, whichever
    1133         * is displayed against the vertical axis for the particular type of plot
    1134         * implementing this interface.
    1135         *
    1136         * @return The maximum value in either the domain or the range
    1137         */
    1138        public Number getMaximumVerticalDataValue() {
    1139            return new Double(this.upperBound);
    1140        }
    1141
    1142        /**
    1143         * Returns the data range.
    1144         *
    1145         * @param axis  the axis.
    1146         *
    1147         * @return The range of data displayed.
    1148         */
    1149        public Range getDataRange(ValueAxis axis) {
    1150           return new Range(this.lowerBound, this.upperBound);
    1151        }
    1152
    1153        /**
    1154         * Sets the axis range to the current values in the rangeInfo array.
    1155         */
    1156        protected void setAxisRange() {
    1157            if ((this.subrange >= 0) && (this.followDataInSubranges)) {
    1158                this.rangeAxis.setRange(
    1159                        new Range(this.subrangeInfo[this.subrange][DISPLAY_LOW],
    1160                        this.subrangeInfo[this.subrange][DISPLAY_HIGH]));
    1161            }
    1162            else {
    1163                this.rangeAxis.setRange(this.lowerBound, this.upperBound);
    1164            }
    1165        }
    1166
    1167        /**
    1168         * Returns the legend items for the plot.
    1169         *
    1170         * @return <code>null</code>.
    1171         */
    1172        public LegendItemCollection getLegendItems() {
    1173            return null;
    1174        }
    1175
    1176        /**
    1177         * Returns the orientation of the plot.
    1178         *
    1179         * @return The orientation (always {@link PlotOrientation#VERTICAL}).
    1180         */
    1181        public PlotOrientation getOrientation() {
    1182            return PlotOrientation.VERTICAL;
    1183        }
    1184
    1185        /**
    1186         * Determine whether a number is valid and finite.
    1187         *
    1188         * @param d  the number to be tested.
    1189         *
    1190         * @return <code>true</code> if the number is valid and finite, and
    1191         *         <code>false</code> otherwise.
    1192         */
    1193        protected static boolean isValidNumber(double d) {
    1194            return (!(Double.isNaN(d) || Double.isInfinite(d)));
    1195        }
    1196
    1197        /**
    1198         * Returns true if the value is in the specified range, and false otherwise.
    1199         *
    1200         * @param subrange  the subrange.
    1201         * @param value  the value to check.
    1202         *
    1203         * @return A boolean.
    1204         */
    1205        private boolean inSubrange(int subrange, double value) {
    1206            return (value > this.subrangeInfo[subrange][RANGE_LOW]
    1207                && value <= this.subrangeInfo[subrange][RANGE_HIGH]);
    1208        }
    1209
    1210        /**
    1211         * Returns the mercury paint corresponding to the current data value.
    1212         *
    1213         * @return The paint.
    1214         */
    1215        private Paint getCurrentPaint() {
    1216
    1217            Paint result = this.mercuryPaint;
    1218            if (this.useSubrangePaint) {
    1219                double value = this.dataset.getValue().doubleValue();
    1220                if (inSubrange(NORMAL, value)) {
    1221                    result = this.subrangePaint[NORMAL];
    1222                }
    1223                else if (inSubrange(WARNING, value)) {
    1224                    result = this.subrangePaint[WARNING];
    1225                }
    1226                else if (inSubrange(CRITICAL, value)) {
    1227                    result = this.subrangePaint[CRITICAL];
    1228                }
    1229            }
    1230            return result;
    1231        }
    1232
    1233        /**
    1234         * Tests this plot for equality with another object.  The plot's dataset
    1235         * is not considered in the test.
    1236         *
    1237         * @param obj  the object (<code>null</code> permitted).
    1238         *
    1239         * @return <code>true</code> or <code>false</code>.
    1240         */
    1241        public boolean equals(Object obj) {
    1242            if (obj == this) {
    1243                return true;
    1244            }
    1245            if (!(obj instanceof ThermometerPlot)) {
    1246                return false;
    1247            }
    1248            ThermometerPlot that = (ThermometerPlot) obj;
    1249            if (!super.equals(obj)) {
    1250                return false;
    1251            }
    1252            if (!ObjectUtilities.equal(this.rangeAxis, that.rangeAxis)) {
    1253                return false;
    1254            }
    1255            if (this.axisLocation != that.axisLocation) {
    1256                return false;
    1257            }
    1258            if (this.lowerBound != that.lowerBound) {
    1259                return false;
    1260            }
    1261            if (this.upperBound != that.upperBound) {
    1262                return false;
    1263            }
    1264            if (!ObjectUtilities.equal(this.padding, that.padding)) {
    1265                return false;
    1266            }
    1267            if (!ObjectUtilities.equal(this.thermometerStroke,
    1268                    that.thermometerStroke)) {
    1269                return false;
    1270            }
    1271            if (!PaintUtilities.equal(this.thermometerPaint,
    1272                    that.thermometerPaint)) {
    1273                return false;
    1274            }
    1275            if (this.units != that.units) {
    1276                return false;
    1277            }
    1278            if (this.valueLocation != that.valueLocation) {
    1279                return false;
    1280            }
    1281            if (!ObjectUtilities.equal(this.valueFont, that.valueFont)) {
    1282                return false;
    1283            }
    1284            if (!PaintUtilities.equal(this.valuePaint, that.valuePaint)) {
    1285                return false;
    1286            }
    1287            if (!ObjectUtilities.equal(this.valueFormat, that.valueFormat)) {
    1288                return false;
    1289            }
    1290            if (!PaintUtilities.equal(this.mercuryPaint, that.mercuryPaint)) {
    1291                return false;
    1292            }
    1293            if (this.showValueLines != that.showValueLines) {
    1294                return false;
    1295            }
    1296            if (this.subrange != that.subrange) {
    1297                return false;
    1298            }
    1299            if (this.followDataInSubranges != that.followDataInSubranges) {
    1300                return false;
    1301            }
    1302            if (!equal(this.subrangeInfo, that.subrangeInfo)) {
    1303                return false;
    1304            }
    1305            if (this.useSubrangePaint != that.useSubrangePaint) {
    1306                return false;
    1307            }
    1308            for (int i = 0; i < this.subrangePaint.length; i++) {
    1309                if (!PaintUtilities.equal(this.subrangePaint[i],
    1310                        that.subrangePaint[i])) {
    1311                    return false;
    1312                }
    1313            }
    1314            return true;
    1315        }
    1316
    1317        /**
    1318         * Tests two double[][] arrays for equality.
    1319         *
    1320         * @param array1  the first array (<code>null</code> permitted).
    1321         * @param array2  the second arrray (<code>null</code> permitted).
    1322         *
    1323         * @return A boolean.
    1324         */
    1325        private static boolean equal(double[][] array1, double[][] array2) {
    1326            if (array1 == null) {
    1327                return (array2 == null);
    1328            }
    1329            if (array2 == null) {
    1330                return false;
    1331            }
    1332            if (array1.length != array2.length) {
    1333                return false;
    1334            }
    1335            for (int i = 0; i < array1.length; i++) {
    1336                if (!Arrays.equals(array1[i], array2[i])) {
    1337                    return false;
    1338                }
    1339            }
    1340            return true;
    1341        }
    1342
    1343        /**
    1344         * Returns a clone of the plot.
    1345         *
    1346         * @return A clone.
    1347         *
    1348         * @throws CloneNotSupportedException  if the plot cannot be cloned.
    1349         */
    1350        public Object clone() throws CloneNotSupportedException {
    1351
    1352            ThermometerPlot clone = (ThermometerPlot) super.clone();
    1353
    1354            if (clone.dataset != null) {
    1355                clone.dataset.addChangeListener(clone);
    1356            }
    1357            clone.rangeAxis = (ValueAxis) ObjectUtilities.clone(this.rangeAxis);
    1358            if (clone.rangeAxis != null) {
    1359                clone.rangeAxis.setPlot(clone);
    1360                clone.rangeAxis.addChangeListener(clone);
    1361            }
    1362            clone.valueFormat = (NumberFormat) this.valueFormat.clone();
    1363            clone.subrangePaint = (Paint[]) this.subrangePaint.clone();
    1364
    1365            return clone;
    1366
    1367        }
    1368
    1369        /**
    1370         * Provides serialization support.
    1371         *
    1372         * @param stream  the output stream.
    1373         *
    1374         * @throws IOException  if there is an I/O error.
    1375         */
    1376        private void writeObject(ObjectOutputStream stream) throws IOException {
    1377            stream.defaultWriteObject();
    1378            SerialUtilities.writeStroke(this.thermometerStroke, stream);
    1379            SerialUtilities.writePaint(this.thermometerPaint, stream);
    1380            SerialUtilities.writePaint(this.valuePaint, stream);
    1381            SerialUtilities.writePaint(this.mercuryPaint, stream);
    1382            SerialUtilities.writeStroke(this.subrangeIndicatorStroke, stream);
    1383            SerialUtilities.writeStroke(this.rangeIndicatorStroke, stream);
    1384        }
    1385
    1386        /**
    1387         * Provides serialization support.
    1388         *
    1389         * @param stream  the input stream.
    1390         *
    1391         * @throws IOException  if there is an I/O error.
    1392         * @throws ClassNotFoundException  if there is a classpath problem.
    1393         */
    1394        private void readObject(ObjectInputStream stream) throws IOException,
    1395                ClassNotFoundException {
    1396            stream.defaultReadObject();
    1397            this.thermometerStroke = SerialUtilities.readStroke(stream);
    1398            this.thermometerPaint = SerialUtilities.readPaint(stream);
    1399            this.valuePaint = SerialUtilities.readPaint(stream);
    1400            this.mercuryPaint = SerialUtilities.readPaint(stream);
    1401            this.subrangeIndicatorStroke = SerialUtilities.readStroke(stream);
    1402            this.rangeIndicatorStroke = SerialUtilities.readStroke(stream);
    1403
    1404            if (this.rangeAxis != null) {
    1405                this.rangeAxis.addChangeListener(this);
    1406            }
    1407        }
    1408
    1409        /**
    1410         * Multiplies the range on the domain axis/axes by the specified factor.
    1411         *
    1412         * @param factor  the zoom factor.
    1413         * @param state  the plot state.
    1414         * @param source  the source point.
    1415         */
    1416        public void zoomDomainAxes(double factor, PlotRenderingInfo state,
    1417                                   Point2D source) {
    1418            // TODO: to be implemented.
    1419        }
    1420
    1421        /**
    1422         * Multiplies the range on the range axis/axes by the specified factor.
    1423         *
    1424         * @param factor  the zoom factor.
    1425         * @param state  the plot state.
    1426         * @param source  the source point.
    1427         */
    1428        public void zoomRangeAxes(double factor, PlotRenderingInfo state,
    1429                                  Point2D source) {
    1430            this.rangeAxis.resizeRange(factor);
    1431        }
    1432
    1433        /**
    1434         * This method does nothing.
    1435         *
    1436         * @param lowerPercent  the lower percent.
    1437         * @param upperPercent  the upper percent.
    1438         * @param state  the plot state.
    1439         * @param source  the source point.
    1440         */
    1441        public void zoomDomainAxes(double lowerPercent, double upperPercent,
    1442                                   PlotRenderingInfo state, Point2D source) {
    1443            // no domain axis to zoom
    1444        }
    1445
    1446        /**
    1447         * Zooms the range axes.
    1448         *
    1449         * @param lowerPercent  the lower percent.
    1450         * @param upperPercent  the upper percent.
    1451         * @param state  the plot state.
    1452         * @param source  the source point.
    1453         */
    1454        public void zoomRangeAxes(double lowerPercent, double upperPercent,
    1455                                  PlotRenderingInfo state, Point2D source) {
    1456            this.rangeAxis.zoomRange(lowerPercent, upperPercent);
    1457        }
    1458
    1459        /**
    1460         * Returns <code>false</code>.
    1461         *
    1462         * @return A boolean.
    1463         */
    1464        public boolean isDomainZoomable() {
    1465            return false;
    1466        }
    1467
    1468        /**
    1469         * Returns <code>true</code>.
    1470         *
    1471         * @return A boolean.
    1472         */
    1473        public boolean isRangeZoomable() {
    1474            return true;
    1475        }
    1476
    1477    }