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