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