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