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