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 }