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