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