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 * DialValueIndicator.java
029 * -----------------------
030 * (C) Copyright 2006-2008, by Object Refinery Limited.
031 *
032 * Original Author:  David Gilbert (for Object Refinery Limited);
033 * Contributor(s):   -;
034 *
035 * Changes
036 * -------
037 * 03-Nov-2006 : Version 1 (DG);
038 * 17-Oct-2007 : Updated equals() (DG);
039 * 24-Oct-2007 : Added default constructor and missing event notification (DG);
040 *
041 */
042
043package org.jfree.chart.plot.dial;
044
045import java.awt.BasicStroke;
046import java.awt.Color;
047import java.awt.Font;
048import java.awt.FontMetrics;
049import java.awt.Graphics2D;
050import java.awt.Paint;
051import java.awt.Stroke;
052import java.awt.geom.Arc2D;
053import java.awt.geom.Point2D;
054import java.awt.geom.Rectangle2D;
055import java.io.IOException;
056import java.io.ObjectInputStream;
057import java.io.ObjectOutputStream;
058import java.io.Serializable;
059import java.text.DecimalFormat;
060import java.text.NumberFormat;
061
062import org.jfree.chart.HashUtilities;
063import org.jfree.io.SerialUtilities;
064import org.jfree.text.TextUtilities;
065import org.jfree.ui.RectangleAnchor;
066import org.jfree.ui.RectangleInsets;
067import org.jfree.ui.Size2D;
068import org.jfree.ui.TextAnchor;
069import org.jfree.util.PaintUtilities;
070import org.jfree.util.PublicCloneable;
071
072/**
073 * A value indicator for a {@link DialPlot}.
074 *
075 * @since 1.0.7
076 */
077public class DialValueIndicator extends AbstractDialLayer implements DialLayer,
078        Cloneable, PublicCloneable, Serializable {
079
080    /** For serialization. */
081    static final long serialVersionUID = 803094354130942585L;
082
083    /** The dataset index. */
084    private int datasetIndex;
085
086    /** The angle that defines the anchor point. */
087    private double angle;
088
089    /** The radius that defines the anchor point. */
090    private double radius;
091
092    /** The frame anchor. */
093    private RectangleAnchor frameAnchor;
094
095    /** The template value. */
096    private Number templateValue;
097
098    /** The formatter. */
099    private NumberFormat formatter;
100
101    /** The font. */
102    private Font font;
103
104    /** The paint. */
105    private transient Paint paint;
106
107    /** The background paint. */
108    private transient Paint backgroundPaint;
109
110    /** The outline stroke. */
111    private transient Stroke outlineStroke;
112
113    /** The outline paint. */
114    private transient Paint outlinePaint;
115
116    /** The insets. */
117    private RectangleInsets insets;
118
119    /** The value anchor. */
120    private RectangleAnchor valueAnchor;
121
122    /** The text anchor for displaying the value. */
123    private TextAnchor textAnchor;
124
125    /**
126     * Creates a new instance of <code>DialValueIndicator</code>.
127     */
128    public DialValueIndicator() {
129        this(0);
130    }
131
132    /**
133     * Creates a new instance of <code>DialValueIndicator</code>.
134     *
135     * @param datasetIndex  the dataset index.
136     */
137    public DialValueIndicator(int datasetIndex) {
138        this.datasetIndex = datasetIndex;
139        this.angle = -90.0;
140        this.radius = 0.3;
141        this.frameAnchor = RectangleAnchor.CENTER;
142        this.templateValue = new Double(100.0);
143        this.formatter = new DecimalFormat("0.0");
144        this.font = new Font("Dialog", Font.BOLD, 14);
145        this.paint = Color.black;
146        this.backgroundPaint = Color.white;
147        this.outlineStroke = new BasicStroke(1.0f);
148        this.outlinePaint = Color.blue;
149        this.insets = new RectangleInsets(4, 4, 4, 4);
150        this.valueAnchor = RectangleAnchor.RIGHT;
151        this.textAnchor = TextAnchor.CENTER_RIGHT;
152    }
153
154    /**
155     * Returns the index of the dataset from which this indicator fetches its
156     * current value.
157     *
158     * @return The dataset index.
159     *
160     * @see #setDatasetIndex(int)
161     */
162    public int getDatasetIndex() {
163        return this.datasetIndex;
164    }
165
166    /**
167     * Sets the dataset index and sends a {@link DialLayerChangeEvent} to all
168     * registered listeners.
169     *
170     * @param index  the index.
171     *
172     * @see #getDatasetIndex()
173     */
174    public void setDatasetIndex(int index) {
175        this.datasetIndex = index;
176        notifyListeners(new DialLayerChangeEvent(this));
177    }
178
179    /**
180     * Returns the angle for the anchor point.  The angle is specified in
181     * degrees using the same orientation as Java's <code>Arc2D</code> class.
182     *
183     * @return The angle (in degrees).
184     *
185     * @see #setAngle(double)
186     */
187    public double getAngle() {
188        return this.angle;
189    }
190
191    /**
192     * Sets the angle for the anchor point and sends a
193     * {@link DialLayerChangeEvent} to all registered listeners.
194     *
195     * @param angle  the angle (in degrees).
196     *
197     * @see #getAngle()
198     */
199    public void setAngle(double angle) {
200        this.angle = angle;
201        notifyListeners(new DialLayerChangeEvent(this));
202    }
203
204    /**
205     * Returns the radius.
206     *
207     * @return The radius.
208     *
209     * @see #setRadius(double)
210     */
211    public double getRadius() {
212        return this.radius;
213    }
214
215    /**
216     * Sets the radius and sends a {@link DialLayerChangeEvent} to all
217     * registered listeners.
218     *
219     * @param radius  the radius.
220     *
221     * @see #getRadius()
222     */
223    public void setRadius(double radius) {
224        this.radius = radius;
225        notifyListeners(new DialLayerChangeEvent(this));
226    }
227
228    /**
229     * Returns the frame anchor.
230     *
231     * @return The frame anchor.
232     *
233     * @see #setFrameAnchor(RectangleAnchor)
234     */
235    public RectangleAnchor getFrameAnchor() {
236        return this.frameAnchor;
237    }
238
239    /**
240     * Sets the frame anchor and sends a {@link DialLayerChangeEvent} to all
241     * registered listeners.
242     *
243     * @param anchor  the anchor (<code>null</code> not permitted).
244     *
245     * @see #getFrameAnchor()
246     */
247    public void setFrameAnchor(RectangleAnchor anchor) {
248        if (anchor == null) {
249            throw new IllegalArgumentException("Null 'anchor' argument.");
250        }
251        this.frameAnchor = anchor;
252        notifyListeners(new DialLayerChangeEvent(this));
253    }
254
255    /**
256     * Returns the template value.
257     *
258     * @return The template value (never <code>null</code>).
259     *
260     * @see #setTemplateValue(Number)
261     */
262    public Number getTemplateValue() {
263        return this.templateValue;
264    }
265
266    /**
267     * Sets the template value and sends a {@link DialLayerChangeEvent} to
268     * all registered listeners.
269     *
270     * @param value  the value (<code>null</code> not permitted).
271     *
272     * @see #setTemplateValue(Number)
273     */
274    public void setTemplateValue(Number value) {
275        if (value == null) {
276            throw new IllegalArgumentException("Null 'value' argument.");
277        }
278        this.templateValue = value;
279        notifyListeners(new DialLayerChangeEvent(this));
280    }
281
282    /**
283     * Returns the formatter used to format the value.
284     *
285     * @return The formatter (never <code>null</code>).
286     *
287     * @see #setNumberFormat(NumberFormat)
288     */
289    public NumberFormat getNumberFormat() {
290        return this.formatter;
291    }
292
293    /**
294     * Sets the formatter used to format the value and sends a
295     * {@link DialLayerChangeEvent} to all registered listeners.
296     *
297     * @param formatter  the formatter (<code>null</code> not permitted).
298     *
299     * @see #getNumberFormat()
300     */
301    public void setNumberFormat(NumberFormat formatter) {
302        if (formatter == null) {
303            throw new IllegalArgumentException("Null 'formatter' argument.");
304        }
305        this.formatter = formatter;
306        notifyListeners(new DialLayerChangeEvent(this));
307    }
308
309    /**
310     * Returns the font.
311     *
312     * @return The font (never <code>null</code>).
313     *
314     * @see #getFont()
315     */
316    public Font getFont() {
317        return this.font;
318    }
319
320    /**
321     * Sets the font and sends a {@link DialLayerChangeEvent} to all registered
322     * listeners.
323     *
324     * @param font  the font (<code>null</code> not permitted).
325     */
326    public void setFont(Font font) {
327        if (font == null) {
328            throw new IllegalArgumentException("Null 'font' argument.");
329        }
330        this.font = font;
331        notifyListeners(new DialLayerChangeEvent(this));
332    }
333
334    /**
335     * Returns the paint.
336     *
337     * @return The paint (never <code>null</code>).
338     *
339     * @see #setPaint(Paint)
340     */
341    public Paint getPaint() {
342        return this.paint;
343    }
344
345    /**
346     * Sets the paint and sends a {@link DialLayerChangeEvent} to all
347     * registered listeners.
348     *
349     * @param paint  the paint (<code>null</code> not permitted).
350     *
351     * @see #getPaint()
352     */
353    public void setPaint(Paint paint) {
354        if (paint == null) {
355            throw new IllegalArgumentException("Null 'paint' argument.");
356        }
357        this.paint = paint;
358        notifyListeners(new DialLayerChangeEvent(this));
359    }
360
361    /**
362     * Returns the background paint.
363     *
364     * @return The background paint.
365     *
366     * @see #setBackgroundPaint(Paint)
367     */
368    public Paint getBackgroundPaint() {
369        return this.backgroundPaint;
370    }
371
372    /**
373     * Sets the background paint and sends a {@link DialLayerChangeEvent} to
374     * all registered listeners.
375     *
376     * @param paint  the paint (<code>null</code> not permitted).
377     *
378     * @see #getBackgroundPaint()
379     */
380    public void setBackgroundPaint(Paint paint) {
381        if (paint == null) {
382            throw new IllegalArgumentException("Null 'paint' argument.");
383        }
384        this.backgroundPaint = paint;
385        notifyListeners(new DialLayerChangeEvent(this));
386    }
387
388    /**
389     * Returns the outline stroke.
390     *
391     * @return The outline stroke (never <code>null</code>).
392     *
393     * @see #setOutlineStroke(Stroke)
394     */
395    public Stroke getOutlineStroke() {
396        return this.outlineStroke;
397    }
398
399    /**
400     * Sets the outline stroke and sends a {@link DialLayerChangeEvent} to
401     * all registered listeners.
402     *
403     * @param stroke  the stroke (<code>null</code> not permitted).
404     *
405     * @see #getOutlineStroke()
406     */
407    public void setOutlineStroke(Stroke stroke) {
408        if (stroke == null) {
409            throw new IllegalArgumentException("Null 'stroke' argument.");
410        }
411        this.outlineStroke = stroke;
412        notifyListeners(new DialLayerChangeEvent(this));
413    }
414
415    /**
416     * Returns the outline paint.
417     *
418     * @return The outline paint (never <code>null</code>).
419     *
420     * @see #setOutlinePaint(Paint)
421     */
422    public Paint getOutlinePaint() {
423        return this.outlinePaint;
424    }
425
426    /**
427     * Sets the outline paint and sends a {@link DialLayerChangeEvent} to all
428     * registered listeners.
429     *
430     * @param paint  the paint (<code>null</code> not permitted).
431     *
432     * @see #getOutlinePaint()
433     */
434    public void setOutlinePaint(Paint paint) {
435        if (paint == null) {
436            throw new IllegalArgumentException("Null 'paint' argument.");
437        }
438        this.outlinePaint = paint;
439        notifyListeners(new DialLayerChangeEvent(this));
440    }
441
442    /**
443     * Returns the insets.
444     *
445     * @return The insets (never <code>null</code>).
446     *
447     * @see #setInsets(RectangleInsets)
448     */
449    public RectangleInsets getInsets() {
450        return this.insets;
451    }
452
453    /**
454     * Sets the insets and sends a {@link DialLayerChangeEvent} to all
455     * registered listeners.
456     *
457     * @param insets  the insets (<code>null</code> not permitted).
458     *
459     * @see #getInsets()
460     */
461    public void setInsets(RectangleInsets insets) {
462        if (insets == null) {
463            throw new IllegalArgumentException("Null 'insets' argument.");
464        }
465        this.insets = insets;
466        notifyListeners(new DialLayerChangeEvent(this));
467    }
468
469    /**
470     * Returns the value anchor.
471     *
472     * @return The value anchor (never <code>null</code>).
473     *
474     * @see #setValueAnchor(RectangleAnchor)
475     */
476    public RectangleAnchor getValueAnchor() {
477        return this.valueAnchor;
478    }
479
480    /**
481     * Sets the value anchor and sends a {@link DialLayerChangeEvent} to all
482     * registered listeners.
483     *
484     * @param anchor  the anchor (<code>null</code> not permitted).
485     *
486     * @see #getValueAnchor()
487     */
488    public void setValueAnchor(RectangleAnchor anchor) {
489        if (anchor == null) {
490            throw new IllegalArgumentException("Null 'anchor' argument.");
491        }
492        this.valueAnchor = anchor;
493        notifyListeners(new DialLayerChangeEvent(this));
494    }
495
496    /**
497     * Returns the text anchor.
498     *
499     * @return The text anchor (never <code>null</code>).
500     *
501     * @see #setTextAnchor(TextAnchor)
502     */
503    public TextAnchor getTextAnchor() {
504        return this.textAnchor;
505    }
506
507    /**
508     * Sets the text anchor and sends a {@link DialLayerChangeEvent} to all
509     * registered listeners.
510     *
511     * @param anchor  the anchor (<code>null</code> not permitted).
512     *
513     * @see #getTextAnchor()
514     */
515    public void setTextAnchor(TextAnchor anchor) {
516        if (anchor == null) {
517            throw new IllegalArgumentException("Null 'anchor' argument.");
518        }
519        this.textAnchor = anchor;
520        notifyListeners(new DialLayerChangeEvent(this));
521    }
522
523    /**
524     * Returns <code>true</code> to indicate that this layer should be
525     * clipped within the dial window.
526     *
527     * @return <code>true</code>.
528     */
529    public boolean isClippedToWindow() {
530        return true;
531    }
532
533    /**
534     * Draws the background to the specified graphics device.  If the dial
535     * frame specifies a window, the clipping region will already have been
536     * set to this window before this method is called.
537     *
538     * @param g2  the graphics device (<code>null</code> not permitted).
539     * @param plot  the plot (ignored here).
540     * @param frame  the dial frame (ignored here).
541     * @param view  the view rectangle (<code>null</code> not permitted).
542     */
543    public void draw(Graphics2D g2, DialPlot plot, Rectangle2D frame,
544            Rectangle2D view) {
545
546        // work out the anchor point
547        Rectangle2D f = DialPlot.rectangleByRadius(frame, this.radius,
548                this.radius);
549        Arc2D arc = new Arc2D.Double(f, this.angle, 0.0, Arc2D.OPEN);
550        Point2D pt = arc.getStartPoint();
551
552        // calculate the bounds of the template value
553        FontMetrics fm = g2.getFontMetrics(this.font);
554        String s = this.formatter.format(this.templateValue);
555        Rectangle2D tb = TextUtilities.getTextBounds(s, g2, fm);
556
557        // align this rectangle to the frameAnchor
558        Rectangle2D bounds = RectangleAnchor.createRectangle(new Size2D(
559                tb.getWidth(), tb.getHeight()), pt.getX(), pt.getY(),
560                this.frameAnchor);
561
562        // add the insets
563        Rectangle2D fb = this.insets.createOutsetRectangle(bounds);
564
565        // draw the background
566        g2.setPaint(this.backgroundPaint);
567        g2.fill(fb);
568
569        // draw the border
570        g2.setStroke(this.outlineStroke);
571        g2.setPaint(this.outlinePaint);
572        g2.draw(fb);
573
574
575        // now find the text anchor point
576        double value = plot.getValue(this.datasetIndex);
577        String valueStr = this.formatter.format(value);
578        Point2D pt2 = RectangleAnchor.coordinates(bounds, this.valueAnchor);
579        g2.setPaint(this.paint);
580        g2.setFont(this.font);
581        TextUtilities.drawAlignedString(valueStr, g2, (float) pt2.getX(),
582                (float) pt2.getY(), this.textAnchor);
583
584    }
585
586    /**
587     * Tests this instance for equality with an arbitrary object.
588     *
589     * @param obj  the object (<code>null</code> permitted).
590     *
591     * @return A boolean.
592     */
593    public boolean equals(Object obj) {
594        if (obj == this) {
595            return true;
596        }
597        if (!(obj instanceof DialValueIndicator)) {
598            return false;
599        }
600        DialValueIndicator that = (DialValueIndicator) obj;
601        if (this.datasetIndex != that.datasetIndex) {
602            return false;
603        }
604        if (this.angle != that.angle) {
605            return false;
606        }
607        if (this.radius != that.radius) {
608            return false;
609        }
610        if (!this.frameAnchor.equals(that.frameAnchor)) {
611            return false;
612        }
613        if (!this.templateValue.equals(that.templateValue)) {
614            return false;
615        }
616        if (!this.font.equals(that.font)) {
617            return false;
618        }
619        if (!PaintUtilities.equal(this.paint, that.paint)) {
620            return false;
621        }
622        if (!PaintUtilities.equal(this.backgroundPaint, that.backgroundPaint)) {
623            return false;
624        }
625        if (!this.outlineStroke.equals(that.outlineStroke)) {
626            return false;
627        }
628        if (!PaintUtilities.equal(this.outlinePaint, that.outlinePaint)) {
629            return false;
630        }
631        if (!this.insets.equals(that.insets)) {
632            return false;
633        }
634        if (!this.valueAnchor.equals(that.valueAnchor)) {
635            return false;
636        }
637        if (!this.textAnchor.equals(that.textAnchor)) {
638            return false;
639        }
640
641        return super.equals(obj);
642    }
643
644    /**
645     * Returns a hash code for this instance.
646     *
647     * @return The hash code.
648     */
649    public int hashCode() {
650        int result = 193;
651        result = 37 * result + HashUtilities.hashCodeForPaint(this.paint);
652        result = 37 * result + HashUtilities.hashCodeForPaint(
653                this.backgroundPaint);
654        result = 37 * result + HashUtilities.hashCodeForPaint(
655                this.outlinePaint);
656        result = 37 * result + this.outlineStroke.hashCode();
657        return result;
658    }
659
660    /**
661     * Returns a clone of this instance.
662     *
663     * @return The clone.
664     *
665     * @throws CloneNotSupportedException if some attribute of this instance
666     *     cannot be cloned.
667     */
668    public Object clone() throws CloneNotSupportedException {
669        return super.clone();
670    }
671
672    /**
673     * Provides serialization support.
674     *
675     * @param stream  the output stream.
676     *
677     * @throws IOException  if there is an I/O error.
678     */
679    private void writeObject(ObjectOutputStream stream) throws IOException {
680        stream.defaultWriteObject();
681        SerialUtilities.writePaint(this.paint, stream);
682        SerialUtilities.writePaint(this.backgroundPaint, stream);
683        SerialUtilities.writePaint(this.outlinePaint, stream);
684        SerialUtilities.writeStroke(this.outlineStroke, stream);
685    }
686
687    /**
688     * Provides serialization support.
689     *
690     * @param stream  the input stream.
691     *
692     * @throws IOException  if there is an I/O error.
693     * @throws ClassNotFoundException  if there is a classpath problem.
694     */
695    private void readObject(ObjectInputStream stream)
696            throws IOException, ClassNotFoundException {
697        stream.defaultReadObject();
698        this.paint = SerialUtilities.readPaint(stream);
699        this.backgroundPaint = SerialUtilities.readPaint(stream);
700        this.outlinePaint = SerialUtilities.readPaint(stream);
701        this.outlineStroke = SerialUtilities.readStroke(stream);
702    }
703
704}