001/* ===========================================================
002 * JFreeChart : a free chart library for the Java(tm) platform
003 * ===========================================================
004 *
005 * (C) Copyright 2000-2022, 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-2021, 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 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.ArrayList;
065import java.util.Collections;
066import java.util.List;
067import java.util.Objects;
068import java.util.ResourceBundle;
069
070import org.jfree.chart.legend.LegendItem;
071import org.jfree.chart.legend.LegendItemCollection;
072import org.jfree.chart.event.PlotChangeEvent;
073import org.jfree.chart.text.TextUtils;
074import org.jfree.chart.api.RectangleInsets;
075import org.jfree.chart.text.TextAnchor;
076import org.jfree.chart.internal.PaintUtils;
077import org.jfree.chart.internal.Args;
078import org.jfree.chart.internal.SerialUtils;
079import org.jfree.data.Range;
080import org.jfree.data.general.DatasetChangeEvent;
081import org.jfree.data.general.ValueDataset;
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            dataset.addChangeListener(this);
690        }
691
692        // send a dataset change event to self...
693        DatasetChangeEvent event = new DatasetChangeEvent(this, dataset);
694        datasetChanged(event);
695
696    }
697
698    /**
699     * Returns an unmodifiable list of the intervals for the plot.
700     *
701     * @return A list.
702     *
703     * @see #addInterval(MeterInterval)
704     */
705    public List<MeterInterval> getIntervals() {
706        return Collections.unmodifiableList(intervals);
707    }
708
709    /**
710     * Adds an interval and sends a {@link PlotChangeEvent} to all registered
711     * listeners.
712     *
713     * @param interval  the interval ({@code null} not permitted).
714     *
715     * @see #getIntervals()
716     * @see #clearIntervals()
717     */
718    public void addInterval(MeterInterval interval) {
719        Args.nullNotPermitted(interval, "interval");
720        intervals.add(interval);
721        fireChangeEvent();
722    }
723
724    /**
725     * Clears the intervals for the plot and sends a {@link PlotChangeEvent} to
726     * all registered listeners.
727     *
728     * @see #addInterval(MeterInterval)
729     */
730    public void clearIntervals() {
731        intervals.clear();
732        fireChangeEvent();
733    }
734
735    /**
736     * Returns an item for each interval.
737     *
738     * @return A collection of legend items.
739     */
740    @Override
741    public LegendItemCollection getLegendItems() {
742        LegendItemCollection result = new LegendItemCollection();
743        for (MeterInterval mi : intervals) {
744            Paint color = mi.getBackgroundPaint();
745            if (color == null) {
746                color = mi.getOutlinePaint();
747            }
748            LegendItem item = new LegendItem(mi.getLabel(), mi.getLabel(),
749                    null, null, new Rectangle2D.Double(-4.0, -4.0, 8.0, 8.0),
750                    color);
751            item.setDataset(getDataset());
752            result.add(item);
753        }
754        return result;
755    }
756
757    /**
758     * Draws the plot on a Java 2D graphics device (such as the screen or a
759     * printer).
760     *
761     * @param g2  the graphics device.
762     * @param area  the area within which the plot should be drawn.
763     * @param anchor  the anchor point ({@code null} permitted).
764     * @param parentState  the state from the parent plot, if there is one.
765     * @param info  collects info about the drawing.
766     */
767    @Override
768    public void draw(Graphics2D g2, Rectangle2D area, Point2D anchor,
769                     PlotState parentState, PlotRenderingInfo info) {
770
771        if (info != null) {
772            info.setPlotArea(area);
773        }
774
775        // adjust for insets...
776        RectangleInsets insets = getInsets();
777        insets.trim(area);
778
779        area.setRect(area.getX() + 4, area.getY() + 4, area.getWidth() - 8,
780                area.getHeight() - 8);
781
782        // draw the background
783        if (this.drawBorder) {
784            drawBackground(g2, area);
785        }
786
787        // adjust the plot area by the interior spacing value
788        double gapHorizontal = (2 * DEFAULT_BORDER_SIZE);
789        double gapVertical = (2 * DEFAULT_BORDER_SIZE);
790        double meterX = area.getX() + gapHorizontal / 2;
791        double meterY = area.getY() + gapVertical / 2;
792        double meterW = area.getWidth() - gapHorizontal;
793        double meterH = area.getHeight() - gapVertical
794                + ((this.meterAngle <= 180) && (this.shape != DialShape.CIRCLE)
795                ? area.getHeight() / 1.25 : 0);
796
797        double min = Math.min(meterW, meterH) / 2;
798        meterX = (meterX + meterX + meterW) / 2 - min;
799        meterY = (meterY + meterY + meterH) / 2 - min;
800        meterW = 2 * min;
801        meterH = 2 * min;
802
803        Rectangle2D meterArea = new Rectangle2D.Double(meterX, meterY, meterW,
804                meterH);
805
806        Rectangle2D.Double originalArea = new Rectangle2D.Double(
807                meterArea.getX() - 4, meterArea.getY() - 4,
808                meterArea.getWidth() + 8, meterArea.getHeight() + 8);
809
810        double meterMiddleX = meterArea.getCenterX();
811        double meterMiddleY = meterArea.getCenterY();
812
813        // plot the data (unless the dataset is null)...
814        ValueDataset data = getDataset();
815        if (data != null) {
816            double dataMin = this.range.getLowerBound();
817            double dataMax = this.range.getUpperBound();
818
819            Shape savedClip = g2.getClip();
820            g2.clip(originalArea);
821            Composite originalComposite = g2.getComposite();
822            g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER,
823                    getForegroundAlpha()));
824
825            if (this.dialBackgroundPaint != null) {
826                fillArc(g2, originalArea, dataMin, dataMax,
827                        this.dialBackgroundPaint, true);
828            }
829            drawTicks(g2, meterArea, dataMin, dataMax);
830            drawArcForInterval(g2, meterArea, new MeterInterval("", this.range,
831                    this.dialOutlinePaint, new BasicStroke(1.0f), null));
832
833            for (MeterInterval interval : this.intervals) {
834                drawArcForInterval(g2, meterArea, interval);
835            }
836
837            Number n = data.getValue();
838            if (n != null) {
839                double value = n.doubleValue();
840                drawValueLabel(g2, meterArea);
841
842                if (this.range.contains(value)) {
843                    g2.setPaint(this.needlePaint);
844                    g2.setStroke(new BasicStroke(2.0f));
845
846                    double radius = (meterArea.getWidth() / 2)
847                                    + DEFAULT_BORDER_SIZE + 15;
848                    double valueAngle = valueToAngle(value);
849                    double valueP1 = meterMiddleX
850                            + (radius * Math.cos(Math.PI * (valueAngle / 180)));
851                    double valueP2 = meterMiddleY
852                            - (radius * Math.sin(Math.PI * (valueAngle / 180)));
853
854                    Polygon arrow = new Polygon();
855                    if ((valueAngle > 135 && valueAngle < 225)
856                        || (valueAngle < 45 && valueAngle > -45)) {
857
858                        double valueP3 = (meterMiddleY
859                                - DEFAULT_CIRCLE_SIZE / 4);
860                        double valueP4 = (meterMiddleY
861                                + DEFAULT_CIRCLE_SIZE / 4);
862                        arrow.addPoint((int) meterMiddleX, (int) valueP3);
863                        arrow.addPoint((int) meterMiddleX, (int) valueP4);
864
865                    }
866                    else {
867                        arrow.addPoint((int) (meterMiddleX
868                                - DEFAULT_CIRCLE_SIZE / 4), (int) meterMiddleY);
869                        arrow.addPoint((int) (meterMiddleX
870                                + DEFAULT_CIRCLE_SIZE / 4), (int) meterMiddleY);
871                    }
872                    arrow.addPoint((int) valueP1, (int) valueP2);
873                    g2.fill(arrow);
874
875                    Ellipse2D circle = new Ellipse2D.Double(meterMiddleX
876                            - DEFAULT_CIRCLE_SIZE / 2, meterMiddleY
877                            - DEFAULT_CIRCLE_SIZE / 2, DEFAULT_CIRCLE_SIZE,
878                            DEFAULT_CIRCLE_SIZE);
879                    g2.fill(circle);
880                }
881            }
882
883            g2.setClip(savedClip);
884            g2.setComposite(originalComposite);
885
886        }
887        if (this.drawBorder) {
888            drawOutline(g2, area);
889        }
890
891    }
892
893    /**
894     * Draws the arc to represent an interval.
895     *
896     * @param g2  the graphics device.
897     * @param meterArea  the drawing area.
898     * @param interval  the interval.
899     */
900    protected void drawArcForInterval(Graphics2D g2, Rectangle2D meterArea,
901                                      MeterInterval interval) {
902
903        double minValue = interval.getRange().getLowerBound();
904        double maxValue = interval.getRange().getUpperBound();
905        Paint outlinePaint = interval.getOutlinePaint();
906        Stroke outlineStroke = interval.getOutlineStroke();
907        Paint backgroundPaint = interval.getBackgroundPaint();
908
909        if (backgroundPaint != null) {
910            fillArc(g2, meterArea, minValue, maxValue, backgroundPaint, false);
911        }
912        if (outlinePaint != null) {
913            if (outlineStroke != null) {
914                drawArc(g2, meterArea, minValue, maxValue, outlinePaint,
915                        outlineStroke);
916            }
917            drawTick(g2, meterArea, minValue, true);
918            drawTick(g2, meterArea, maxValue, true);
919        }
920    }
921
922    /**
923     * Draws an arc.
924     *
925     * @param g2  the graphics device.
926     * @param area  the plot area.
927     * @param minValue  the minimum value.
928     * @param maxValue  the maximum value.
929     * @param paint  the paint.
930     * @param stroke  the stroke.
931     */
932    protected void drawArc(Graphics2D g2, Rectangle2D area, double minValue,
933                           double maxValue, Paint paint, Stroke stroke) {
934
935        double startAngle = valueToAngle(maxValue);
936        double endAngle = valueToAngle(minValue);
937        double extent = endAngle - startAngle;
938
939        double x = area.getX();
940        double y = area.getY();
941        double w = area.getWidth();
942        double h = area.getHeight();
943        g2.setPaint(paint);
944        g2.setStroke(stroke);
945
946        if (paint != null && stroke != null) {
947            Arc2D.Double arc = new Arc2D.Double(x, y, w, h, startAngle,
948                    extent, Arc2D.OPEN);
949            g2.setPaint(paint);
950            g2.setStroke(stroke);
951            g2.draw(arc);
952        }
953
954    }
955
956    /**
957     * Fills an arc on the dial between the given values.
958     *
959     * @param g2  the graphics device.
960     * @param area  the plot area.
961     * @param minValue  the minimum data value.
962     * @param maxValue  the maximum data value.
963     * @param paint  the background paint ({@code null} not permitted).
964     * @param dial  a flag that indicates whether the arc represents the whole
965     *              dial.
966     */
967    protected void fillArc(Graphics2D g2, Rectangle2D area,
968            double minValue, double maxValue, Paint paint, boolean dial) {
969
970        Args.nullNotPermitted(paint, "paint");
971        double startAngle = valueToAngle(maxValue);
972        double endAngle = valueToAngle(minValue);
973        double extent = endAngle - startAngle;
974
975        double x = area.getX();
976        double y = area.getY();
977        double w = area.getWidth();
978        double h = area.getHeight();
979        int joinType = Arc2D.OPEN;
980        if (this.shape == DialShape.PIE) {
981            joinType = Arc2D.PIE;
982        }
983        else if (this.shape == DialShape.CHORD) {
984            if (dial && this.meterAngle > 180) {
985                joinType = Arc2D.CHORD;
986            }
987            else {
988                joinType = Arc2D.PIE;
989            }
990        }
991        else if (this.shape == DialShape.CIRCLE) {
992            joinType = Arc2D.PIE;
993            if (dial) {
994                extent = 360;
995            }
996        }
997        else {
998            throw new IllegalStateException("DialShape not recognised.");
999        }
1000
1001        g2.setPaint(paint);
1002        Arc2D.Double arc = new Arc2D.Double(x, y, w, h, startAngle, extent,
1003                joinType);
1004        g2.fill(arc);
1005    }
1006
1007    /**
1008     * Translates a data value to an angle on the dial.
1009     *
1010     * @param value  the value.
1011     *
1012     * @return The angle on the dial.
1013     */
1014    public double valueToAngle(double value) {
1015        value = value - this.range.getLowerBound();
1016        double baseAngle = 180 + ((this.meterAngle - 180) / 2.0);
1017        return baseAngle - ((value / this.range.getLength()) * this.meterAngle);
1018    }
1019
1020    /**
1021     * Draws the ticks that subdivide the overall range.
1022     *
1023     * @param g2  the graphics device.
1024     * @param meterArea  the meter area.
1025     * @param minValue  the minimum value.
1026     * @param maxValue  the maximum value.
1027     */
1028    protected void drawTicks(Graphics2D g2, Rectangle2D meterArea,
1029                             double minValue, double maxValue) {
1030        for (double v = minValue; v <= maxValue; v += this.tickSize) {
1031            drawTick(g2, meterArea, v);
1032        }
1033    }
1034
1035    /**
1036     * Draws a tick.
1037     *
1038     * @param g2  the graphics device.
1039     * @param meterArea  the meter area.
1040     * @param value  the value.
1041     */
1042    protected void drawTick(Graphics2D g2, Rectangle2D meterArea,
1043            double value) {
1044        drawTick(g2, meterArea, value, false);
1045    }
1046
1047    /**
1048     * Draws a tick on the dial.
1049     *
1050     * @param g2  the graphics device.
1051     * @param meterArea  the meter area.
1052     * @param value  the tick value.
1053     * @param label  a flag that controls whether or not a value label is drawn.
1054     */
1055    protected void drawTick(Graphics2D g2, Rectangle2D meterArea,
1056                            double value, boolean label) {
1057
1058        double valueAngle = valueToAngle(value);
1059
1060        double meterMiddleX = meterArea.getCenterX();
1061        double meterMiddleY = meterArea.getCenterY();
1062
1063        g2.setPaint(this.tickPaint);
1064        g2.setStroke(new BasicStroke(2.0f));
1065
1066        double valueP2X;
1067        double valueP2Y;
1068
1069        double radius = (meterArea.getWidth() / 2) + DEFAULT_BORDER_SIZE;
1070        double radius1 = radius - 15;
1071
1072        double valueP1X = meterMiddleX
1073                + (radius * Math.cos(Math.PI * (valueAngle / 180)));
1074        double valueP1Y = meterMiddleY
1075                - (radius * Math.sin(Math.PI * (valueAngle / 180)));
1076
1077        valueP2X = meterMiddleX
1078                + (radius1 * Math.cos(Math.PI * (valueAngle / 180)));
1079        valueP2Y = meterMiddleY
1080                - (radius1 * Math.sin(Math.PI * (valueAngle / 180)));
1081
1082        Line2D.Double line = new Line2D.Double(valueP1X, valueP1Y, valueP2X,
1083                valueP2Y);
1084        g2.draw(line);
1085
1086        if (this.tickLabelsVisible && label) {
1087
1088            String tickLabel =  this.tickLabelFormat.format(value);
1089            g2.setFont(this.tickLabelFont);
1090            g2.setPaint(this.tickLabelPaint);
1091
1092            FontMetrics fm = g2.getFontMetrics();
1093            Rectangle2D tickLabelBounds
1094                = TextUtils.getTextBounds(tickLabel, g2, fm);
1095
1096            double x = valueP2X;
1097            double y = valueP2Y;
1098            if (valueAngle == 90 || valueAngle == 270) {
1099                x = x - tickLabelBounds.getWidth() / 2;
1100            }
1101            else if (valueAngle < 90 || valueAngle > 270) {
1102                x = x - tickLabelBounds.getWidth();
1103            }
1104            if ((valueAngle > 135 && valueAngle < 225)
1105                    || valueAngle > 315 || valueAngle < 45) {
1106                y = y - tickLabelBounds.getHeight() / 2;
1107            }
1108            else {
1109                y = y + tickLabelBounds.getHeight() / 2;
1110            }
1111            g2.drawString(tickLabel, (float) x, (float) y);
1112        }
1113    }
1114
1115    /**
1116     * Draws the value label just below the center of the dial.
1117     *
1118     * @param g2  the graphics device.
1119     * @param area  the plot area.
1120     */
1121    protected void drawValueLabel(Graphics2D g2, Rectangle2D area) {
1122        if (valueVisible) {
1123            g2.setFont(this.valueFont);
1124            g2.setPaint(this.valuePaint);
1125            String valueStr = "No value";
1126            if (this.dataset != null) {
1127                Number n = this.dataset.getValue();
1128                if (n != null) {
1129                    valueStr = this.tickLabelFormat.format(n.doubleValue()) + " "
1130                        + this.units;
1131                }
1132            }
1133            float x = (float) area.getCenterX();
1134            float y = (float) area.getCenterY() + DEFAULT_CIRCLE_SIZE;
1135            TextUtils.drawAlignedString(valueStr, g2, x, y,
1136                TextAnchor.TOP_CENTER);
1137        }
1138    }
1139
1140    /**
1141     * Returns a short string describing the type of plot.
1142     *
1143     * @return A string describing the type of plot.
1144     */
1145    @Override
1146    public String getPlotType() {
1147        return localizationResources.getString("Meter_Plot");
1148    }
1149
1150    /**
1151     * A zoom method that does nothing.  Plots are required to support the
1152     * zoom operation.  In the case of a meter plot, it doesn't make sense to
1153     * zoom in or out, so the method is empty.
1154     *
1155     * @param percent   The zoom percentage.
1156     */
1157    @Override
1158    public void zoom(double percent) {
1159        // intentionally blank
1160    }
1161
1162    /**
1163     * Tests the plot for equality with an arbitrary object.  Note that the
1164     * dataset is ignored for the purposes of testing equality.
1165     *
1166     * @param obj  the object ({@code null} permitted).
1167     *
1168     * @return A boolean.
1169     */
1170    @Override
1171    public boolean equals(Object obj) {
1172        if (obj == this) {
1173            return true;
1174        }
1175        if (!(obj instanceof MeterPlot)) {
1176            return false;
1177        }
1178        if (!super.equals(obj)) {
1179            return false;
1180        }
1181        MeterPlot that = (MeterPlot) obj;
1182        if (!Objects.equals(this.units, that.units)) {
1183            return false;
1184        }
1185        if (!Objects.equals(this.range, that.range)) {
1186            return false;
1187        }
1188        if (!Objects.equals(this.intervals, that.intervals)) {
1189            return false;
1190        }
1191        if (!PaintUtils.equal(this.dialOutlinePaint,
1192                that.dialOutlinePaint)) {
1193            return false;
1194        }
1195        if (this.shape != that.shape) {
1196            return false;
1197        }
1198        if (!PaintUtils.equal(this.dialBackgroundPaint,
1199                that.dialBackgroundPaint)) {
1200            return false;
1201        }
1202        if (!PaintUtils.equal(this.needlePaint, that.needlePaint)) {
1203            return false;
1204        }
1205        if (this.valueVisible != that.valueVisible) {
1206            return false;
1207        }
1208        if (!Objects.equals(this.valueFont, that.valueFont)) {
1209            return false;
1210        }
1211        if (!PaintUtils.equal(this.valuePaint, that.valuePaint)) {
1212            return false;
1213        }
1214        if (!PaintUtils.equal(this.tickPaint, that.tickPaint)) {
1215            return false;
1216        }
1217        if (this.tickSize != that.tickSize) {
1218            return false;
1219        }
1220        if (this.tickLabelsVisible != that.tickLabelsVisible) {
1221            return false;
1222        }
1223        if (!Objects.equals(this.tickLabelFont, that.tickLabelFont)) {
1224            return false;
1225        }
1226        if (!PaintUtils.equal(this.tickLabelPaint, that.tickLabelPaint)) {
1227            return false;
1228        }
1229        if (!Objects.equals(this.tickLabelFormat, that.tickLabelFormat)) {
1230            return false;
1231        }
1232        if (this.drawBorder != that.drawBorder) {
1233            return false;
1234        }
1235        if (this.meterAngle != that.meterAngle) {
1236            return false;
1237        }
1238        return true;
1239    }
1240
1241    /**
1242     * Provides serialization support.
1243     *
1244     * @param stream  the output stream.
1245     *
1246     * @throws IOException  if there is an I/O error.
1247     */
1248    private void writeObject(ObjectOutputStream stream) throws IOException {
1249        stream.defaultWriteObject();
1250        SerialUtils.writePaint(this.dialBackgroundPaint, stream);
1251        SerialUtils.writePaint(this.dialOutlinePaint, stream);
1252        SerialUtils.writePaint(this.needlePaint, stream);
1253        SerialUtils.writePaint(this.valuePaint, stream);
1254        SerialUtils.writePaint(this.tickPaint, stream);
1255        SerialUtils.writePaint(this.tickLabelPaint, stream);
1256    }
1257
1258    /**
1259     * Provides serialization support.
1260     *
1261     * @param stream  the input stream.
1262     *
1263     * @throws IOException  if there is an I/O error.
1264     * @throws ClassNotFoundException  if there is a classpath problem.
1265     */
1266    private void readObject(ObjectInputStream stream)
1267        throws IOException, ClassNotFoundException {
1268        stream.defaultReadObject();
1269        this.dialBackgroundPaint = SerialUtils.readPaint(stream);
1270        this.dialOutlinePaint = SerialUtils.readPaint(stream);
1271        this.needlePaint = SerialUtils.readPaint(stream);
1272        this.valuePaint = SerialUtils.readPaint(stream);
1273        this.tickPaint = SerialUtils.readPaint(stream);
1274        this.tickLabelPaint = SerialUtils.readPaint(stream);
1275        if (this.dataset != null) {
1276            this.dataset.addChangeListener(this);
1277        }
1278    }
1279
1280    /**
1281     * Returns an independent copy (clone) of the plot.  The dataset is NOT
1282     * cloned - both the original and the clone will have a reference to the
1283     * same dataset.
1284     *
1285     * @return A clone.
1286     *
1287     * @throws CloneNotSupportedException if some component of the plot cannot
1288     *         be cloned.
1289     */
1290    @Override
1291    public Object clone() throws CloneNotSupportedException {
1292        MeterPlot clone = (MeterPlot) super.clone();
1293        clone.tickLabelFormat = (NumberFormat) this.tickLabelFormat.clone();
1294        // the following relies on the fact that the intervals are immutable
1295        clone.intervals = new ArrayList<>(this.intervals);
1296        if (clone.dataset != null) {
1297            clone.dataset.addChangeListener(clone);
1298        }
1299        return clone;
1300    }
1301
1302}