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