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