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