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