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