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