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 * MeterPlot.java
029 * --------------
030 * (C) Copyright 2000-2007, by Hari and Contributors.
031 *
032 * Original Author: Hari (ourhari@hotmail.com);
033 * Contributor(s): David Gilbert (for Object Refinery Limited);
034 * Bob Orchard;
035 * Arnaud Lelievre;
036 * Nicolas Brodu;
037 * David Bastend;
038 *
039 * $Id: MeterPlot.java,v 1.13.2.10 2007/05/18 10:28:21 mungady Exp $
040 *
041 * Changes
042 * -------
043 * 01-Apr-2002 : Version 1, contributed by Hari (DG);
044 * 23-Apr-2002 : Moved dataset from JFreeChart to Plot (DG);
045 * 22-Aug-2002 : Added changes suggest by Bob Orchard, changed Color to Paint
046 * for consistency, plus added Javadoc comments (DG);
047 * 01-Oct-2002 : Fixed errors reported by Checkstyle (DG);
048 * 23-Jan-2003 : Removed one constructor (DG);
049 * 26-Mar-2003 : Implemented Serializable (DG);
050 * 20-Aug-2003 : Changed dataset from MeterDataset --> ValueDataset, added
051 * equals() method,
052 * 08-Sep-2003 : Added internationalization via use of properties
053 * resourceBundle (RFE 690236) (AL);
054 * implemented Cloneable, and various other changes (DG);
055 * 08-Sep-2003 : Added serialization methods (NB);
056 * 11-Sep-2003 : Added cloning support (NB);
057 * 16-Sep-2003 : Changed ChartRenderingInfo --> PlotRenderingInfo (DG);
058 * 25-Sep-2003 : Fix useless cloning. Correct dataset listener registration in
059 * constructor. (NB)
060 * 29-Oct-2003 : Added workaround for font alignment in PDF output (DG);
061 * 17-Jan-2004 : Changed to allow dialBackgroundPaint to be set to null - see
062 * bug 823628 (DG);
063 * 07-Apr-2004 : Changed string bounds calculation (DG);
064 * 12-May-2004 : Added tickLabelFormat attribute - see RFE 949566. Also
065 * updated the equals() method (DG);
066 * 02-Nov-2004 : Added sanity checks for range, and only draw the needle if the
067 * value is contained within the overall range - see bug report
068 * 1056047 (DG);
069 * 11-Jan-2005 : Removed deprecated code in preparation for the 1.0.0
070 * release (DG);
071 * 02-Feb-2005 : Added optional background paint for each region (DG);
072 * 22-Mar-2005 : Removed 'normal', 'warning' and 'critical' regions and put in
073 * facility to define an arbitrary number of MeterIntervals,
074 * based on a contribution by David Bastend (DG);
075 * 20-Apr-2005 : Small update for change to LegendItem constructors (DG);
076 * 05-May-2005 : Updated draw() method parameters (DG);
077 * 08-Jun-2005 : Fixed equals() method to handle GradientPaint (DG);
078 * 10-Nov-2005 : Added tickPaint, tickSize and valuePaint attributes, and
079 * put value label drawing code into a separate method (DG);
080 * ------------- JFREECHART 1.0.x ---------------------------------------------
081 * 05-Mar-2007 : Restore clip region correctly (see bug 1667750) (DG);
082 * 18-May-2007 : Set dataset for LegendItem (DG);
083 *
084 */
085
086 package org.jfree.chart.plot;
087
088 import java.awt.AlphaComposite;
089 import java.awt.BasicStroke;
090 import java.awt.Color;
091 import java.awt.Composite;
092 import java.awt.Font;
093 import java.awt.FontMetrics;
094 import java.awt.Graphics2D;
095 import java.awt.Paint;
096 import java.awt.Polygon;
097 import java.awt.Shape;
098 import java.awt.Stroke;
099 import java.awt.geom.Arc2D;
100 import java.awt.geom.Ellipse2D;
101 import java.awt.geom.Line2D;
102 import java.awt.geom.Point2D;
103 import java.awt.geom.Rectangle2D;
104 import java.io.IOException;
105 import java.io.ObjectInputStream;
106 import java.io.ObjectOutputStream;
107 import java.io.Serializable;
108 import java.text.NumberFormat;
109 import java.util.Collections;
110 import java.util.Iterator;
111 import java.util.List;
112 import java.util.ResourceBundle;
113
114 import org.jfree.chart.LegendItem;
115 import org.jfree.chart.LegendItemCollection;
116 import org.jfree.chart.event.PlotChangeEvent;
117 import org.jfree.data.Range;
118 import org.jfree.data.general.DatasetChangeEvent;
119 import org.jfree.data.general.ValueDataset;
120 import org.jfree.io.SerialUtilities;
121 import org.jfree.text.TextUtilities;
122 import org.jfree.ui.RectangleInsets;
123 import org.jfree.ui.TextAnchor;
124 import org.jfree.util.ObjectUtilities;
125 import org.jfree.util.PaintUtilities;
126
127 /**
128 * A plot that displays a single value in the form of a needle on a dial.
129 * Defined ranges (for example, 'normal', 'warning' and 'critical') can be
130 * highlighted on the dial.
131 */
132 public class MeterPlot extends Plot implements Serializable, Cloneable {
133
134 /** For serialization. */
135 private static final long serialVersionUID = 2987472457734470962L;
136
137 /** The default background paint. */
138 static final Paint DEFAULT_DIAL_BACKGROUND_PAINT = Color.black;
139
140 /** The default needle paint. */
141 static final Paint DEFAULT_NEEDLE_PAINT = Color.green;
142
143 /** The default value font. */
144 static final Font DEFAULT_VALUE_FONT = new Font("SansSerif", Font.BOLD, 12);
145
146 /** The default value paint. */
147 static final Paint DEFAULT_VALUE_PAINT = Color.yellow;
148
149 /** The default meter angle. */
150 public static final int DEFAULT_METER_ANGLE = 270;
151
152 /** The default border size. */
153 public static final float DEFAULT_BORDER_SIZE = 3f;
154
155 /** The default circle size. */
156 public static final float DEFAULT_CIRCLE_SIZE = 10f;
157
158 /** The default label font. */
159 public static final Font DEFAULT_LABEL_FONT = new Font("SansSerif",
160 Font.BOLD, 10);
161
162 /** The dataset (contains a single value). */
163 private ValueDataset dataset;
164
165 /** The dial shape (background shape). */
166 private DialShape shape;
167
168 /** The dial extent (measured in degrees). */
169 private int meterAngle;
170
171 /** The overall range of data values on the dial. */
172 private Range range;
173
174 /** The tick size. */
175 private double tickSize;
176
177 /** The paint used to draw the ticks. */
178 private transient Paint tickPaint;
179
180 /** The units displayed on the dial. */
181 private String units;
182
183 /** The font for the value displayed in the center of the dial. */
184 private Font valueFont;
185
186 /** The paint for the value displayed in the center of the dial. */
187 private transient Paint valuePaint;
188
189 /** A flag that controls whether or not the border is drawn. */
190 private boolean drawBorder;
191
192 /** The outline paint. */
193 private transient Paint dialOutlinePaint;
194
195 /** The paint for the dial background. */
196 private transient Paint dialBackgroundPaint;
197
198 /** The paint for the needle. */
199 private transient Paint needlePaint;
200
201 /** A flag that controls whether or not the tick labels are visible. */
202 private boolean tickLabelsVisible;
203
204 /** The tick label font. */
205 private Font tickLabelFont;
206
207 /** The tick label paint. */
208 private transient Paint tickLabelPaint;
209
210 /** The tick label format. */
211 private NumberFormat tickLabelFormat;
212
213 /** The resourceBundle for the localization. */
214 protected static ResourceBundle localizationResources =
215 ResourceBundle.getBundle("org.jfree.chart.plot.LocalizationBundle");
216
217 /**
218 * A (possibly empty) list of the {@link MeterInterval}s to be highlighted
219 * on the dial.
220 */
221 private List intervals;
222
223 /**
224 * Creates a new plot with a default range of <code>0</code> to
225 * <code>100</code> and no value to display.
226 */
227 public MeterPlot() {
228 this(null);
229 }
230
231 /**
232 * Creates a new plot that displays the value from the supplied dataset.
233 *
234 * @param dataset the dataset (<code>null</code> permitted).
235 */
236 public MeterPlot(ValueDataset dataset) {
237 super();
238 this.shape = DialShape.CIRCLE;
239 this.meterAngle = DEFAULT_METER_ANGLE;
240 this.range = new Range(0.0, 100.0);
241 this.tickSize = 10.0;
242 this.tickPaint = Color.white;
243 this.units = "Units";
244 this.needlePaint = MeterPlot.DEFAULT_NEEDLE_PAINT;
245 this.tickLabelsVisible = true;
246 this.tickLabelFont = MeterPlot.DEFAULT_LABEL_FONT;
247 this.tickLabelPaint = Color.black;
248 this.tickLabelFormat = NumberFormat.getInstance();
249 this.valueFont = MeterPlot.DEFAULT_VALUE_FONT;
250 this.valuePaint = MeterPlot.DEFAULT_VALUE_PAINT;
251 this.dialBackgroundPaint = MeterPlot.DEFAULT_DIAL_BACKGROUND_PAINT;
252 this.intervals = new java.util.ArrayList();
253 setDataset(dataset);
254 }
255
256 /**
257 * Returns the dial shape. The default is {@link DialShape#CIRCLE}).
258 *
259 * @return The dial shape (never <code>null</code>).
260 *
261 * @see #setDialShape(DialShape)
262 */
263 public DialShape getDialShape() {
264 return this.shape;
265 }
266
267 /**
268 * Sets the dial shape and sends a {@link PlotChangeEvent} to all
269 * registered listeners.
270 *
271 * @param shape the shape (<code>null</code> not permitted).
272 *
273 * @see #getDialShape()
274 */
275 public void setDialShape(DialShape shape) {
276 if (shape == null) {
277 throw new IllegalArgumentException("Null 'shape' argument.");
278 }
279 this.shape = shape;
280 notifyListeners(new PlotChangeEvent(this));
281 }
282
283 /**
284 * Returns the meter angle in degrees. This defines, in part, the shape
285 * of the dial. The default is 270 degrees.
286 *
287 * @return The meter angle (in degrees).
288 *
289 * @see #setMeterAngle(int)
290 */
291 public int getMeterAngle() {
292 return this.meterAngle;
293 }
294
295 /**
296 * Sets the angle (in degrees) for the whole range of the dial and sends
297 * a {@link PlotChangeEvent} to all registered listeners.
298 *
299 * @param angle the angle (in degrees, in the range 1-360).
300 *
301 * @see #getMeterAngle()
302 */
303 public void setMeterAngle(int angle) {
304 if (angle < 1 || angle > 360) {
305 throw new IllegalArgumentException("Invalid 'angle' (" + angle
306 + ")");
307 }
308 this.meterAngle = angle;
309 notifyListeners(new PlotChangeEvent(this));
310 }
311
312 /**
313 * Returns the overall range for the dial.
314 *
315 * @return The overall range (never <code>null</code>).
316 *
317 * @see #setRange(Range)
318 */
319 public Range getRange() {
320 return this.range;
321 }
322
323 /**
324 * Sets the range for the dial and sends a {@link PlotChangeEvent} to all
325 * registered listeners.
326 *
327 * @param range the range (<code>null</code> not permitted and zero-length
328 * ranges not permitted).
329 *
330 * @see #getRange()
331 */
332 public void setRange(Range range) {
333 if (range == null) {
334 throw new IllegalArgumentException("Null 'range' argument.");
335 }
336 if (!(range.getLength() > 0.0)) {
337 throw new IllegalArgumentException(
338 "Range length must be positive.");
339 }
340 this.range = range;
341 notifyListeners(new PlotChangeEvent(this));
342 }
343
344 /**
345 * Returns the tick size (the interval between ticks on the dial).
346 *
347 * @return The tick size.
348 *
349 * @see #setTickSize(double)
350 */
351 public double getTickSize() {
352 return this.tickSize;
353 }
354
355 /**
356 * Sets the tick size and sends a {@link PlotChangeEvent} to all
357 * registered listeners.
358 *
359 * @param size the tick size (must be > 0).
360 *
361 * @see #getTickSize()
362 */
363 public void setTickSize(double size) {
364 if (size <= 0) {
365 throw new IllegalArgumentException("Requires 'size' > 0.");
366 }
367 this.tickSize = size;
368 notifyListeners(new PlotChangeEvent(this));
369 }
370
371 /**
372 * Returns the paint used to draw the ticks around the dial.
373 *
374 * @return The paint used to draw the ticks around the dial (never
375 * <code>null</code>).
376 *
377 * @see #setTickPaint(Paint)
378 */
379 public Paint getTickPaint() {
380 return this.tickPaint;
381 }
382
383 /**
384 * Sets the paint used to draw the tick labels around the dial and sends
385 * a {@link PlotChangeEvent} to all registered listeners.
386 *
387 * @param paint the paint (<code>null</code> not permitted).
388 *
389 * @see #getTickPaint()
390 */
391 public void setTickPaint(Paint paint) {
392 if (paint == null) {
393 throw new IllegalArgumentException("Null 'paint' argument.");
394 }
395 this.tickPaint = paint;
396 notifyListeners(new PlotChangeEvent(this));
397 }
398
399 /**
400 * Returns a string describing the units for the dial.
401 *
402 * @return The units (possibly <code>null</code>).
403 *
404 * @see #setUnits(String)
405 */
406 public String getUnits() {
407 return this.units;
408 }
409
410 /**
411 * Sets the units for the dial and sends a {@link PlotChangeEvent} to all
412 * registered listeners.
413 *
414 * @param units the units (<code>null</code> permitted).
415 *
416 * @see #getUnits()
417 */
418 public void setUnits(String units) {
419 this.units = units;
420 notifyListeners(new PlotChangeEvent(this));
421 }
422
423 /**
424 * Returns the paint for the needle.
425 *
426 * @return The paint (never <code>null</code>).
427 *
428 * @see #setNeedlePaint(Paint)
429 */
430 public Paint getNeedlePaint() {
431 return this.needlePaint;
432 }
433
434 /**
435 * Sets the paint used to display the needle and sends a
436 * {@link PlotChangeEvent} to all registered listeners.
437 *
438 * @param paint the paint (<code>null</code> not permitted).
439 *
440 * @see #getNeedlePaint()
441 */
442 public void setNeedlePaint(Paint paint) {
443 if (paint == null) {
444 throw new IllegalArgumentException("Null 'paint' argument.");
445 }
446 this.needlePaint = paint;
447 notifyListeners(new PlotChangeEvent(this));
448 }
449
450 /**
451 * Returns the flag that determines whether or not tick labels are visible.
452 *
453 * @return The flag.
454 *
455 * @see #setTickLabelsVisible(boolean)
456 */
457 public boolean getTickLabelsVisible() {
458 return this.tickLabelsVisible;
459 }
460
461 /**
462 * Sets the flag that controls whether or not the tick labels are visible
463 * and sends a {@link PlotChangeEvent} to all registered listeners.
464 *
465 * @param visible the flag.
466 *
467 * @see #getTickLabelsVisible()
468 */
469 public void setTickLabelsVisible(boolean visible) {
470 if (this.tickLabelsVisible != visible) {
471 this.tickLabelsVisible = visible;
472 notifyListeners(new PlotChangeEvent(this));
473 }
474 }
475
476 /**
477 * Returns the tick label font.
478 *
479 * @return The font (never <code>null</code>).
480 *
481 * @see #setTickLabelFont(Font)
482 */
483 public Font getTickLabelFont() {
484 return this.tickLabelFont;
485 }
486
487 /**
488 * Sets the tick label font and sends a {@link PlotChangeEvent} to all
489 * registered listeners.
490 *
491 * @param font the font (<code>null</code> not permitted).
492 *
493 * @see #getTickLabelFont()
494 */
495 public void setTickLabelFont(Font font) {
496 if (font == null) {
497 throw new IllegalArgumentException("Null 'font' argument.");
498 }
499 if (!this.tickLabelFont.equals(font)) {
500 this.tickLabelFont = font;
501 notifyListeners(new PlotChangeEvent(this));
502 }
503 }
504
505 /**
506 * Returns the tick label paint.
507 *
508 * @return The paint (never <code>null</code>).
509 *
510 * @see #setTickLabelPaint(Paint)
511 */
512 public Paint getTickLabelPaint() {
513 return this.tickLabelPaint;
514 }
515
516 /**
517 * Sets the tick label paint and sends a {@link PlotChangeEvent} to all
518 * registered listeners.
519 *
520 * @param paint the paint (<code>null</code> not permitted).
521 *
522 * @see #getTickLabelPaint()
523 */
524 public void setTickLabelPaint(Paint paint) {
525 if (paint == null) {
526 throw new IllegalArgumentException("Null 'paint' argument.");
527 }
528 if (!this.tickLabelPaint.equals(paint)) {
529 this.tickLabelPaint = paint;
530 notifyListeners(new PlotChangeEvent(this));
531 }
532 }
533
534 /**
535 * Returns the tick label format.
536 *
537 * @return The tick label format (never <code>null</code>).
538 *
539 * @see #setTickLabelFormat(NumberFormat)
540 */
541 public NumberFormat getTickLabelFormat() {
542 return this.tickLabelFormat;
543 }
544
545 /**
546 * Sets the format for the tick labels and sends a {@link PlotChangeEvent}
547 * to all registered listeners.
548 *
549 * @param format the format (<code>null</code> not permitted).
550 *
551 * @see #getTickLabelFormat()
552 */
553 public void setTickLabelFormat(NumberFormat format) {
554 if (format == null) {
555 throw new IllegalArgumentException("Null 'format' argument.");
556 }
557 this.tickLabelFormat = format;
558 notifyListeners(new PlotChangeEvent(this));
559 }
560
561 /**
562 * Returns the font for the value label.
563 *
564 * @return The font (never <code>null</code>).
565 *
566 * @see #setValueFont(Font)
567 */
568 public Font getValueFont() {
569 return this.valueFont;
570 }
571
572 /**
573 * Sets the font used to display the value label and sends a
574 * {@link PlotChangeEvent} to all registered listeners.
575 *
576 * @param font the font (<code>null</code> not permitted).
577 *
578 * @see #getValueFont()
579 */
580 public void setValueFont(Font font) {
581 if (font == null) {
582 throw new IllegalArgumentException("Null 'font' argument.");
583 }
584 this.valueFont = font;
585 notifyListeners(new PlotChangeEvent(this));
586 }
587
588 /**
589 * Returns the paint for the value label.
590 *
591 * @return The paint (never <code>null</code>).
592 *
593 * @see #setValuePaint(Paint)
594 */
595 public Paint getValuePaint() {
596 return this.valuePaint;
597 }
598
599 /**
600 * Sets the paint used to display the value label and sends a
601 * {@link PlotChangeEvent} to all registered listeners.
602 *
603 * @param paint the paint (<code>null</code> not permitted).
604 *
605 * @see #getValuePaint()
606 */
607 public void setValuePaint(Paint paint) {
608 if (paint == null) {
609 throw new IllegalArgumentException("Null 'paint' argument.");
610 }
611 this.valuePaint = paint;
612 notifyListeners(new PlotChangeEvent(this));
613 }
614
615 /**
616 * Returns the paint for the dial background.
617 *
618 * @return The paint (possibly <code>null</code>).
619 *
620 * @see #setDialBackgroundPaint(Paint)
621 */
622 public Paint getDialBackgroundPaint() {
623 return this.dialBackgroundPaint;
624 }
625
626 /**
627 * Sets the paint used to fill the dial background. Set this to
628 * <code>null</code> for no background.
629 *
630 * @param paint the paint (<code>null</code> permitted).
631 *
632 * @see #getDialBackgroundPaint()
633 */
634 public void setDialBackgroundPaint(Paint paint) {
635 this.dialBackgroundPaint = paint;
636 notifyListeners(new PlotChangeEvent(this));
637 }
638
639 /**
640 * Returns a flag that controls whether or not a rectangular border is
641 * drawn around the plot area.
642 *
643 * @return A flag.
644 *
645 * @see #setDrawBorder(boolean)
646 */
647 public boolean getDrawBorder() {
648 return this.drawBorder;
649 }
650
651 /**
652 * Sets the flag that controls whether or not a rectangular border is drawn
653 * around the plot area and sends a {@link PlotChangeEvent} to all
654 * registered listeners.
655 *
656 * @param draw the flag.
657 *
658 * @see #getDrawBorder()
659 */
660 public void setDrawBorder(boolean draw) {
661 // TODO: fix output when this flag is set to true
662 this.drawBorder = draw;
663 notifyListeners(new PlotChangeEvent(this));
664 }
665
666 /**
667 * Returns the dial outline paint.
668 *
669 * @return The paint.
670 *
671 * @see #setDialOutlinePaint(Paint)
672 */
673 public Paint getDialOutlinePaint() {
674 return this.dialOutlinePaint;
675 }
676
677 /**
678 * Sets the dial outline paint and sends a {@link PlotChangeEvent} to all
679 * registered listeners.
680 *
681 * @param paint the paint.
682 *
683 * @see #getDialOutlinePaint()
684 */
685 public void setDialOutlinePaint(Paint paint) {
686 this.dialOutlinePaint = paint;
687 notifyListeners(new PlotChangeEvent(this));
688 }
689
690 /**
691 * Returns the dataset for the plot.
692 *
693 * @return The dataset (possibly <code>null</code>).
694 *
695 * @see #setDataset(ValueDataset)
696 */
697 public ValueDataset getDataset() {
698 return this.dataset;
699 }
700
701 /**
702 * Sets the dataset for the plot, replacing the existing dataset if there
703 * is one, and triggers a {@link PlotChangeEvent}.
704 *
705 * @param dataset the dataset (<code>null</code> permitted).
706 *
707 * @see #getDataset()
708 */
709 public void setDataset(ValueDataset dataset) {
710
711 // if there is an existing dataset, remove the plot from the list of
712 // change listeners...
713 ValueDataset existing = this.dataset;
714 if (existing != null) {
715 existing.removeChangeListener(this);
716 }
717
718 // set the new dataset, and register the chart as a change listener...
719 this.dataset = dataset;
720 if (dataset != null) {
721 setDatasetGroup(dataset.getGroup());
722 dataset.addChangeListener(this);
723 }
724
725 // send a dataset change event to self...
726 DatasetChangeEvent event = new DatasetChangeEvent(this, dataset);
727 datasetChanged(event);
728
729 }
730
731 /**
732 * Returns an unmodifiable list of the intervals for the plot.
733 *
734 * @return A list.
735 *
736 * @see #addInterval(MeterInterval)
737 */
738 public List getIntervals() {
739 return Collections.unmodifiableList(this.intervals);
740 }
741
742 /**
743 * Adds an interval and sends a {@link PlotChangeEvent} to all registered
744 * listeners.
745 *
746 * @param interval the interval (<code>null</code> not permitted).
747 *
748 * @see #getIntervals()
749 * @see #clearIntervals()
750 */
751 public void addInterval(MeterInterval interval) {
752 if (interval == null) {
753 throw new IllegalArgumentException("Null 'interval' argument.");
754 }
755 this.intervals.add(interval);
756 notifyListeners(new PlotChangeEvent(this));
757 }
758
759 /**
760 * Clears the intervals for the plot and sends a {@link PlotChangeEvent} to
761 * all registered listeners.
762 *
763 * @see #addInterval(MeterInterval)
764 */
765 public void clearIntervals() {
766 this.intervals.clear();
767 notifyListeners(new PlotChangeEvent(this));
768 }
769
770 /**
771 * Returns an item for each interval.
772 *
773 * @return A collection of legend items.
774 */
775 public LegendItemCollection getLegendItems() {
776 LegendItemCollection result = new LegendItemCollection();
777 Iterator iterator = this.intervals.iterator();
778 while (iterator.hasNext()) {
779 MeterInterval mi = (MeterInterval) iterator.next();
780 Paint color = mi.getBackgroundPaint();
781 if (color == null) {
782 color = mi.getOutlinePaint();
783 }
784 LegendItem item = new LegendItem(mi.getLabel(), mi.getLabel(),
785 null, null, new Rectangle2D.Double(-4.0, -4.0, 8.0, 8.0),
786 color);
787 item.setDataset(getDataset());
788 result.add(item);
789 }
790 return result;
791 }
792
793 /**
794 * Draws the plot on a Java 2D graphics device (such as the screen or a
795 * printer).
796 *
797 * @param g2 the graphics device.
798 * @param area the area within which the plot should be drawn.
799 * @param anchor the anchor point (<code>null</code> permitted).
800 * @param parentState the state from the parent plot, if there is one.
801 * @param info collects info about the drawing.
802 */
803 public void draw(Graphics2D g2, Rectangle2D area, Point2D anchor,
804 PlotState parentState,
805 PlotRenderingInfo info) {
806
807 if (info != null) {
808 info.setPlotArea(area);
809 }
810
811 // adjust for insets...
812 RectangleInsets insets = getInsets();
813 insets.trim(area);
814
815 area.setRect(area.getX() + 4, area.getY() + 4, area.getWidth() - 8,
816 area.getHeight() - 8);
817
818 // draw the background
819 if (this.drawBorder) {
820 drawBackground(g2, area);
821 }
822
823 // adjust the plot area by the interior spacing value
824 double gapHorizontal = (2 * DEFAULT_BORDER_SIZE);
825 double gapVertical = (2 * DEFAULT_BORDER_SIZE);
826 double meterX = area.getX() + gapHorizontal / 2;
827 double meterY = area.getY() + gapVertical / 2;
828 double meterW = area.getWidth() - gapHorizontal;
829 double meterH = area.getHeight() - gapVertical
830 + ((this.meterAngle <= 180) && (this.shape != DialShape.CIRCLE)
831 ? area.getHeight() / 1.25 : 0);
832
833 double min = Math.min(meterW, meterH) / 2;
834 meterX = (meterX + meterX + meterW) / 2 - min;
835 meterY = (meterY + meterY + meterH) / 2 - min;
836 meterW = 2 * min;
837 meterH = 2 * min;
838
839 Rectangle2D meterArea = new Rectangle2D.Double(meterX, meterY, meterW,
840 meterH);
841
842 Rectangle2D.Double originalArea = new Rectangle2D.Double(
843 meterArea.getX() - 4, meterArea.getY() - 4,
844 meterArea.getWidth() + 8, meterArea.getHeight() + 8);
845
846 double meterMiddleX = meterArea.getCenterX();
847 double meterMiddleY = meterArea.getCenterY();
848
849 // plot the data (unless the dataset is null)...
850 ValueDataset data = getDataset();
851 if (data != null) {
852 double dataMin = this.range.getLowerBound();
853 double dataMax = this.range.getUpperBound();
854
855 Shape savedClip = g2.getClip();
856 g2.clip(originalArea);
857 Composite originalComposite = g2.getComposite();
858 g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER,
859 getForegroundAlpha()));
860
861 if (this.dialBackgroundPaint != null) {
862 fillArc(g2, originalArea, dataMin, dataMax,
863 this.dialBackgroundPaint, true);
864 }
865 drawTicks(g2, meterArea, dataMin, dataMax);
866 drawArcForInterval(g2, meterArea, new MeterInterval("", this.range,
867 this.dialOutlinePaint, new BasicStroke(1.0f), null));
868
869 Iterator iterator = this.intervals.iterator();
870 while (iterator.hasNext()) {
871 MeterInterval interval = (MeterInterval) iterator.next();
872 drawArcForInterval(g2, meterArea, interval);
873 }
874
875 Number n = data.getValue();
876 if (n != null) {
877 double value = n.doubleValue();
878 drawValueLabel(g2, meterArea);
879
880 if (this.range.contains(value)) {
881 g2.setPaint(this.needlePaint);
882 g2.setStroke(new BasicStroke(2.0f));
883
884 double radius = (meterArea.getWidth() / 2)
885 + DEFAULT_BORDER_SIZE + 15;
886 double valueAngle = valueToAngle(value);
887 double valueP1 = meterMiddleX
888 + (radius * Math.cos(Math.PI * (valueAngle / 180)));
889 double valueP2 = meterMiddleY
890 - (radius * Math.sin(Math.PI * (valueAngle / 180)));
891
892 Polygon arrow = new Polygon();
893 if ((valueAngle > 135 && valueAngle < 225)
894 || (valueAngle < 45 && valueAngle > -45)) {
895
896 double valueP3 = (meterMiddleY
897 - DEFAULT_CIRCLE_SIZE / 4);
898 double valueP4 = (meterMiddleY
899 + DEFAULT_CIRCLE_SIZE / 4);
900 arrow.addPoint((int) meterMiddleX, (int) valueP3);
901 arrow.addPoint((int) meterMiddleX, (int) valueP4);
902
903 }
904 else {
905 arrow.addPoint((int) (meterMiddleX
906 - DEFAULT_CIRCLE_SIZE / 4), (int) meterMiddleY);
907 arrow.addPoint((int) (meterMiddleX
908 + DEFAULT_CIRCLE_SIZE / 4), (int) meterMiddleY);
909 }
910 arrow.addPoint((int) valueP1, (int) valueP2);
911 g2.fill(arrow);
912
913 Ellipse2D circle = new Ellipse2D.Double(meterMiddleX
914 - DEFAULT_CIRCLE_SIZE / 2, meterMiddleY
915 - DEFAULT_CIRCLE_SIZE / 2, DEFAULT_CIRCLE_SIZE,
916 DEFAULT_CIRCLE_SIZE);
917 g2.fill(circle);
918 }
919 }
920
921 g2.setClip(savedClip);
922 g2.setComposite(originalComposite);
923
924 }
925 if (this.drawBorder) {
926 drawOutline(g2, area);
927 }
928
929 }
930
931 /**
932 * Draws the arc to represent an interval.
933 *
934 * @param g2 the graphics device.
935 * @param meterArea the drawing area.
936 * @param interval the interval.
937 */
938 protected void drawArcForInterval(Graphics2D g2, Rectangle2D meterArea,
939 MeterInterval interval) {
940
941 double minValue = interval.getRange().getLowerBound();
942 double maxValue = interval.getRange().getUpperBound();
943 Paint outlinePaint = interval.getOutlinePaint();
944 Stroke outlineStroke = interval.getOutlineStroke();
945 Paint backgroundPaint = interval.getBackgroundPaint();
946
947 if (backgroundPaint != null) {
948 fillArc(g2, meterArea, minValue, maxValue, backgroundPaint, false);
949 }
950 if (outlinePaint != null) {
951 if (outlineStroke != null) {
952 drawArc(g2, meterArea, minValue, maxValue, outlinePaint,
953 outlineStroke);
954 }
955 drawTick(g2, meterArea, minValue, true);
956 drawTick(g2, meterArea, maxValue, true);
957 }
958 }
959
960 /**
961 * Draws an arc.
962 *
963 * @param g2 the graphics device.
964 * @param area the plot area.
965 * @param minValue the minimum value.
966 * @param maxValue the maximum value.
967 * @param paint the paint.
968 * @param stroke the stroke.
969 */
970 protected void drawArc(Graphics2D g2, Rectangle2D area, double minValue,
971 double maxValue, Paint paint, Stroke stroke) {
972
973 double startAngle = valueToAngle(maxValue);
974 double endAngle = valueToAngle(minValue);
975 double extent = endAngle - startAngle;
976
977 double x = area.getX();
978 double y = area.getY();
979 double w = area.getWidth();
980 double h = area.getHeight();
981 g2.setPaint(paint);
982 g2.setStroke(stroke);
983
984 if (paint != null && stroke != null) {
985 Arc2D.Double arc = new Arc2D.Double(x, y, w, h, startAngle,
986 extent, Arc2D.OPEN);
987 g2.setPaint(paint);
988 g2.setStroke(stroke);
989 g2.draw(arc);
990 }
991
992 }
993
994 /**
995 * Fills an arc on the dial between the given values.
996 *
997 * @param g2 the graphics device.
998 * @param area the plot area.
999 * @param minValue the minimum data value.
1000 * @param maxValue the maximum data value.
1001 * @param paint the background paint (<code>null</code> not permitted).
1002 * @param dial a flag that indicates whether the arc represents the whole
1003 * dial.
1004 */
1005 protected void fillArc(Graphics2D g2, Rectangle2D area,
1006 double minValue, double maxValue, Paint paint,
1007 boolean dial) {
1008 if (paint == null) {
1009 throw new IllegalArgumentException("Null 'paint' argument");
1010 }
1011 double startAngle = valueToAngle(maxValue);
1012 double endAngle = valueToAngle(minValue);
1013 double extent = endAngle - startAngle;
1014
1015 double x = area.getX();
1016 double y = area.getY();
1017 double w = area.getWidth();
1018 double h = area.getHeight();
1019 int joinType = Arc2D.OPEN;
1020 if (this.shape == DialShape.PIE) {
1021 joinType = Arc2D.PIE;
1022 }
1023 else if (this.shape == DialShape.CHORD) {
1024 if (dial && this.meterAngle > 180) {
1025 joinType = Arc2D.CHORD;
1026 }
1027 else {
1028 joinType = Arc2D.PIE;
1029 }
1030 }
1031 else if (this.shape == DialShape.CIRCLE) {
1032 joinType = Arc2D.PIE;
1033 if (dial) {
1034 extent = 360;
1035 }
1036 }
1037 else {
1038 throw new IllegalStateException("DialShape not recognised.");
1039 }
1040
1041 g2.setPaint(paint);
1042 Arc2D.Double arc = new Arc2D.Double(x, y, w, h, startAngle, extent,
1043 joinType);
1044 g2.fill(arc);
1045 }
1046
1047 /**
1048 * Translates a data value to an angle on the dial.
1049 *
1050 * @param value the value.
1051 *
1052 * @return The angle on the dial.
1053 */
1054 public double valueToAngle(double value) {
1055 value = value - this.range.getLowerBound();
1056 double baseAngle = 180 + ((this.meterAngle - 180) / 2);
1057 return baseAngle - ((value / this.range.getLength()) * this.meterAngle);
1058 }
1059
1060 /**
1061 * Draws the ticks that subdivide the overall range.
1062 *
1063 * @param g2 the graphics device.
1064 * @param meterArea the meter area.
1065 * @param minValue the minimum value.
1066 * @param maxValue the maximum value.
1067 */
1068 protected void drawTicks(Graphics2D g2, Rectangle2D meterArea,
1069 double minValue, double maxValue) {
1070 for (double v = minValue; v <= maxValue; v += this.tickSize) {
1071 drawTick(g2, meterArea, v);
1072 }
1073 }
1074
1075 /**
1076 * Draws a tick.
1077 *
1078 * @param g2 the graphics device.
1079 * @param meterArea the meter area.
1080 * @param value the value.
1081 */
1082 protected void drawTick(Graphics2D g2, Rectangle2D meterArea,
1083 double value) {
1084 drawTick(g2, meterArea, value, false);
1085 }
1086
1087 /**
1088 * Draws a tick on the dial.
1089 *
1090 * @param g2 the graphics device.
1091 * @param meterArea the meter area.
1092 * @param value the tick value.
1093 * @param label a flag that controls whether or not a value label is drawn.
1094 */
1095 protected void drawTick(Graphics2D g2, Rectangle2D meterArea,
1096 double value, boolean label) {
1097
1098 double valueAngle = valueToAngle(value);
1099
1100 double meterMiddleX = meterArea.getCenterX();
1101 double meterMiddleY = meterArea.getCenterY();
1102
1103 g2.setPaint(this.tickPaint);
1104 g2.setStroke(new BasicStroke(2.0f));
1105
1106 double valueP2X = 0;
1107 double valueP2Y = 0;
1108
1109 double radius = (meterArea.getWidth() / 2) + DEFAULT_BORDER_SIZE;
1110 double radius1 = radius - 15;
1111
1112 double valueP1X = meterMiddleX
1113 + (radius * Math.cos(Math.PI * (valueAngle / 180)));
1114 double valueP1Y = meterMiddleY
1115 - (radius * Math.sin(Math.PI * (valueAngle / 180)));
1116
1117 valueP2X = meterMiddleX
1118 + (radius1 * Math.cos(Math.PI * (valueAngle / 180)));
1119 valueP2Y = meterMiddleY
1120 - (radius1 * Math.sin(Math.PI * (valueAngle / 180)));
1121
1122 Line2D.Double line = new Line2D.Double(valueP1X, valueP1Y, valueP2X,
1123 valueP2Y);
1124 g2.draw(line);
1125
1126 if (this.tickLabelsVisible && label) {
1127
1128 String tickLabel = this.tickLabelFormat.format(value);
1129 g2.setFont(this.tickLabelFont);
1130 g2.setPaint(this.tickLabelPaint);
1131
1132 FontMetrics fm = g2.getFontMetrics();
1133 Rectangle2D tickLabelBounds
1134 = TextUtilities.getTextBounds(tickLabel, g2, fm);
1135
1136 double x = valueP2X;
1137 double y = valueP2Y;
1138 if (valueAngle == 90 || valueAngle == 270) {
1139 x = x - tickLabelBounds.getWidth() / 2;
1140 }
1141 else if (valueAngle < 90 || valueAngle > 270) {
1142 x = x - tickLabelBounds.getWidth();
1143 }
1144 if ((valueAngle > 135 && valueAngle < 225)
1145 || valueAngle > 315 || valueAngle < 45) {
1146 y = y - tickLabelBounds.getHeight() / 2;
1147 }
1148 else {
1149 y = y + tickLabelBounds.getHeight() / 2;
1150 }
1151 g2.drawString(tickLabel, (float) x, (float) y);
1152 }
1153 }
1154
1155 /**
1156 * Draws the value label just below the center of the dial.
1157 *
1158 * @param g2 the graphics device.
1159 * @param area the plot area.
1160 */
1161 protected void drawValueLabel(Graphics2D g2, Rectangle2D area) {
1162 g2.setFont(this.valueFont);
1163 g2.setPaint(this.valuePaint);
1164 String valueStr = "No value";
1165 if (this.dataset != null) {
1166 Number n = this.dataset.getValue();
1167 if (n != null) {
1168 valueStr = this.tickLabelFormat.format(n.doubleValue()) + " "
1169 + this.units;
1170 }
1171 }
1172 float x = (float) area.getCenterX();
1173 float y = (float) area.getCenterY() + DEFAULT_CIRCLE_SIZE;
1174 TextUtilities.drawAlignedString(valueStr, g2, x, y,
1175 TextAnchor.TOP_CENTER);
1176 }
1177
1178 /**
1179 * Returns a short string describing the type of plot.
1180 *
1181 * @return A string describing the type of plot.
1182 */
1183 public String getPlotType() {
1184 return localizationResources.getString("Meter_Plot");
1185 }
1186
1187 /**
1188 * A zoom method that does nothing. Plots are required to support the
1189 * zoom operation. In the case of a meter plot, it doesn't make sense to
1190 * zoom in or out, so the method is empty.
1191 *
1192 * @param percent The zoom percentage.
1193 */
1194 public void zoom(double percent) {
1195 // intentionally blank
1196 }
1197
1198 /**
1199 * Tests the plot for equality with an arbitrary object. Note that the
1200 * dataset is ignored for the purposes of testing equality.
1201 *
1202 * @param obj the object (<code>null</code> permitted).
1203 *
1204 * @return A boolean.
1205 */
1206 public boolean equals(Object obj) {
1207 if (obj == this) {
1208 return true;
1209 }
1210 if (!(obj instanceof MeterPlot)) {
1211 return false;
1212 }
1213 if (!super.equals(obj)) {
1214 return false;
1215 }
1216 MeterPlot that = (MeterPlot) obj;
1217 if (!ObjectUtilities.equal(this.units, that.units)) {
1218 return false;
1219 }
1220 if (!ObjectUtilities.equal(this.range, that.range)) {
1221 return false;
1222 }
1223 if (!ObjectUtilities.equal(this.intervals, that.intervals)) {
1224 return false;
1225 }
1226 if (!PaintUtilities.equal(this.dialOutlinePaint,
1227 that.dialOutlinePaint)) {
1228 return false;
1229 }
1230 if (this.shape != that.shape) {
1231 return false;
1232 }
1233 if (!PaintUtilities.equal(this.dialBackgroundPaint,
1234 that.dialBackgroundPaint)) {
1235 return false;
1236 }
1237 if (!PaintUtilities.equal(this.needlePaint, that.needlePaint)) {
1238 return false;
1239 }
1240 if (!ObjectUtilities.equal(this.valueFont, that.valueFont)) {
1241 return false;
1242 }
1243 if (!PaintUtilities.equal(this.valuePaint, that.valuePaint)) {
1244 return false;
1245 }
1246 if (!PaintUtilities.equal(this.tickPaint, that.tickPaint)) {
1247 return false;
1248 }
1249 if (this.tickSize != that.tickSize) {
1250 return false;
1251 }
1252 if (this.tickLabelsVisible != that.tickLabelsVisible) {
1253 return false;
1254 }
1255 if (!ObjectUtilities.equal(this.tickLabelFont, that.tickLabelFont)) {
1256 return false;
1257 }
1258 if (!PaintUtilities.equal(this.tickLabelPaint, that.tickLabelPaint)) {
1259 return false;
1260 }
1261 if (!ObjectUtilities.equal(this.tickLabelFormat,
1262 that.tickLabelFormat)) {
1263 return false;
1264 }
1265 if (this.drawBorder != that.drawBorder) {
1266 return false;
1267 }
1268 if (this.meterAngle != that.meterAngle) {
1269 return false;
1270 }
1271 return true;
1272 }
1273
1274 /**
1275 * Provides serialization support.
1276 *
1277 * @param stream the output stream.
1278 *
1279 * @throws IOException if there is an I/O error.
1280 */
1281 private void writeObject(ObjectOutputStream stream) throws IOException {
1282 stream.defaultWriteObject();
1283 SerialUtilities.writePaint(this.dialBackgroundPaint, stream);
1284 SerialUtilities.writePaint(this.needlePaint, stream);
1285 SerialUtilities.writePaint(this.valuePaint, stream);
1286 SerialUtilities.writePaint(this.tickPaint, stream);
1287 SerialUtilities.writePaint(this.tickLabelPaint, stream);
1288 }
1289
1290 /**
1291 * Provides serialization support.
1292 *
1293 * @param stream the input stream.
1294 *
1295 * @throws IOException if there is an I/O error.
1296 * @throws ClassNotFoundException if there is a classpath problem.
1297 */
1298 private void readObject(ObjectInputStream stream)
1299 throws IOException, ClassNotFoundException {
1300 stream.defaultReadObject();
1301 this.dialBackgroundPaint = SerialUtilities.readPaint(stream);
1302 this.needlePaint = SerialUtilities.readPaint(stream);
1303 this.valuePaint = SerialUtilities.readPaint(stream);
1304 this.tickPaint = SerialUtilities.readPaint(stream);
1305 this.tickLabelPaint = SerialUtilities.readPaint(stream);
1306 if (this.dataset != null) {
1307 this.dataset.addChangeListener(this);
1308 }
1309 }
1310
1311 /**
1312 * Returns an independent copy (clone) of the plot. The dataset is NOT
1313 * cloned - both the original and the clone will have a reference to the
1314 * same dataset.
1315 *
1316 * @return A clone.
1317 *
1318 * @throws CloneNotSupportedException if some component of the plot cannot
1319 * be cloned.
1320 */
1321 public Object clone() throws CloneNotSupportedException {
1322 MeterPlot clone = (MeterPlot) super.clone();
1323 clone.tickLabelFormat = (NumberFormat) this.tickLabelFormat.clone();
1324 // the following relies on the fact that the intervals are immutable
1325 clone.intervals = new java.util.ArrayList(this.intervals);
1326 if (clone.dataset != null) {
1327 clone.dataset.addChangeListener(clone);
1328 }
1329 return clone;
1330 }
1331
1332 }