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