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