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