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