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
084package org.jfree.chart.plot;
085
086import java.awt.AlphaComposite;
087import java.awt.BasicStroke;
088import java.awt.Color;
089import java.awt.Composite;
090import java.awt.Font;
091import java.awt.FontMetrics;
092import java.awt.Graphics2D;
093import java.awt.Paint;
094import java.awt.Polygon;
095import java.awt.Shape;
096import java.awt.Stroke;
097import java.awt.geom.Arc2D;
098import java.awt.geom.Ellipse2D;
099import java.awt.geom.Line2D;
100import java.awt.geom.Point2D;
101import java.awt.geom.Rectangle2D;
102import java.io.IOException;
103import java.io.ObjectInputStream;
104import java.io.ObjectOutputStream;
105import java.io.Serializable;
106import java.text.NumberFormat;
107import java.util.Collections;
108import java.util.Iterator;
109import java.util.List;
110import java.util.ResourceBundle;
111
112import org.jfree.chart.LegendItem;
113import org.jfree.chart.LegendItemCollection;
114import org.jfree.chart.event.PlotChangeEvent;
115import org.jfree.data.Range;
116import org.jfree.data.general.DatasetChangeEvent;
117import org.jfree.data.general.ValueDataset;
118import org.jfree.io.SerialUtilities;
119import org.jfree.text.TextUtilities;
120import org.jfree.ui.RectangleInsets;
121import org.jfree.ui.TextAnchor;
122import org.jfree.util.ObjectUtilities;
123import 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 */
130public 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}