001/* ===========================================================
002 * JFreeChart : a free chart library for the Java(tm) platform
003 * ===========================================================
004 *
005 * (C) Copyright 2000-2011, by Object Refinery Limited and Contributors.
006 *
007 * Project Info:  http://www.jfree.org/jfreechart/index.html
008 *
009 * This library is free software; you can redistribute it and/or modify it
010 * under the terms of the GNU Lesser General Public License as published by
011 * the Free Software Foundation; either version 2.1 of the License, or
012 * (at your option) any later version.
013 *
014 * This library is distributed in the hope that it will be useful, but
015 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
016 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
017 * License for more details.
018 *
019 * You should have received a copy of the GNU Lesser General Public
020 * License along with this library; if not, write to the Free Software
021 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301,
022 * USA.
023 *
024 * [Oracle and Java are registered trademarks of Oracle and/or its affiliates. 
025 * Other names may be trademarks of their respective owners.]
026 *
027 * -----------------------
028 * DialValueIndicator.java
029 * -----------------------
030 * (C) Copyright 2006-2009, 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 * 09-Jun-2009 : Improved indicator resizing, fixes bug 2802014 (DG);
041 *
042 */
043
044package org.jfree.chart.plot.dial;
045
046import java.awt.BasicStroke;
047import java.awt.Color;
048import java.awt.Font;
049import java.awt.FontMetrics;
050import java.awt.Graphics2D;
051import java.awt.Paint;
052import java.awt.Shape;
053import java.awt.Stroke;
054import java.awt.geom.Arc2D;
055import java.awt.geom.Point2D;
056import java.awt.geom.Rectangle2D;
057import java.io.IOException;
058import java.io.ObjectInputStream;
059import java.io.ObjectOutputStream;
060import java.io.Serializable;
061import java.text.DecimalFormat;
062import java.text.NumberFormat;
063
064import org.jfree.chart.HashUtilities;
065import org.jfree.io.SerialUtilities;
066import org.jfree.text.TextUtilities;
067import org.jfree.ui.RectangleAnchor;
068import org.jfree.ui.RectangleInsets;
069import org.jfree.ui.Size2D;
070import org.jfree.ui.TextAnchor;
071import org.jfree.util.ObjectUtilities;
072import org.jfree.util.PaintUtilities;
073import org.jfree.util.PublicCloneable;
074
075/**
076 * A value indicator for a {@link DialPlot}.
077 *
078 * @since 1.0.7
079 */
080public class DialValueIndicator extends AbstractDialLayer implements DialLayer,
081        Cloneable, PublicCloneable, Serializable {
082
083    /** For serialization. */
084    static final long serialVersionUID = 803094354130942585L;
085
086    /** The dataset index. */
087    private int datasetIndex;
088
089    /** The angle that defines the anchor point. */
090    private double angle;
091
092    /** The radius that defines the anchor point. */
093    private double radius;
094
095    /** The frame anchor. */
096    private RectangleAnchor frameAnchor;
097
098    /** The template value. */
099    private Number templateValue;
100
101    /**
102     * A data value that will be formatted to determine the maximum size of
103     * the indicator bounds.  If this is null, the indicator bounds can grow
104     * as large as necessary to contain the actual data value.
105     *
106     * @since 1.0.14
107     */
108    private Number maxTemplateValue;
109
110    /** The formatter. */
111    private NumberFormat formatter;
112
113    /** The font. */
114    private Font font;
115
116    /** The paint. */
117    private transient Paint paint;
118
119    /** The background paint. */
120    private transient Paint backgroundPaint;
121
122    /** The outline stroke. */
123    private transient Stroke outlineStroke;
124
125    /** The outline paint. */
126    private transient Paint outlinePaint;
127
128    /** The insets. */
129    private RectangleInsets insets;
130
131    /** The value anchor. */
132    private RectangleAnchor valueAnchor;
133
134    /** The text anchor for displaying the value. */
135    private TextAnchor textAnchor;
136
137    /**
138     * Creates a new instance of <code>DialValueIndicator</code>.
139     */
140    public DialValueIndicator() {
141        this(0);
142    }
143
144    /**
145     * Creates a new instance of <code>DialValueIndicator</code>.
146     *
147     * @param datasetIndex  the dataset index.
148     */
149    public DialValueIndicator(int datasetIndex) {
150        this.datasetIndex = datasetIndex;
151        this.angle = -90.0;
152        this.radius = 0.3;
153        this.frameAnchor = RectangleAnchor.CENTER;
154        this.templateValue = new Double(100.0);
155        this.maxTemplateValue = null;
156        this.formatter = new DecimalFormat("0.0");
157        this.font = new Font("Dialog", Font.BOLD, 14);
158        this.paint = Color.black;
159        this.backgroundPaint = Color.white;
160        this.outlineStroke = new BasicStroke(1.0f);
161        this.outlinePaint = Color.blue;
162        this.insets = new RectangleInsets(4, 4, 4, 4);
163        this.valueAnchor = RectangleAnchor.RIGHT;
164        this.textAnchor = TextAnchor.CENTER_RIGHT;
165    }
166
167    /**
168     * Returns the index of the dataset from which this indicator fetches its
169     * current value.
170     *
171     * @return The dataset index.
172     *
173     * @see #setDatasetIndex(int)
174     */
175    public int getDatasetIndex() {
176        return this.datasetIndex;
177    }
178
179    /**
180     * Sets the dataset index and sends a {@link DialLayerChangeEvent} to all
181     * registered listeners.
182     *
183     * @param index  the index.
184     *
185     * @see #getDatasetIndex()
186     */
187    public void setDatasetIndex(int index) {
188        this.datasetIndex = index;
189        notifyListeners(new DialLayerChangeEvent(this));
190    }
191
192    /**
193     * Returns the angle for the anchor point.  The angle is specified in
194     * degrees using the same orientation as Java's <code>Arc2D</code> class.
195     *
196     * @return The angle (in degrees).
197     *
198     * @see #setAngle(double)
199     */
200    public double getAngle() {
201        return this.angle;
202    }
203
204    /**
205     * Sets the angle for the anchor point and sends a
206     * {@link DialLayerChangeEvent} to all registered listeners.
207     *
208     * @param angle  the angle (in degrees).
209     *
210     * @see #getAngle()
211     */
212    public void setAngle(double angle) {
213        this.angle = angle;
214        notifyListeners(new DialLayerChangeEvent(this));
215    }
216
217    /**
218     * Returns the radius.
219     *
220     * @return The radius.
221     *
222     * @see #setRadius(double)
223     */
224    public double getRadius() {
225        return this.radius;
226    }
227
228    /**
229     * Sets the radius and sends a {@link DialLayerChangeEvent} to all
230     * registered listeners.
231     *
232     * @param radius  the radius.
233     *
234     * @see #getRadius()
235     */
236    public void setRadius(double radius) {
237        this.radius = radius;
238        notifyListeners(new DialLayerChangeEvent(this));
239    }
240
241    /**
242     * Returns the frame anchor.
243     *
244     * @return The frame anchor.
245     *
246     * @see #setFrameAnchor(RectangleAnchor)
247     */
248    public RectangleAnchor getFrameAnchor() {
249        return this.frameAnchor;
250    }
251
252    /**
253     * Sets the frame anchor and sends a {@link DialLayerChangeEvent} to all
254     * registered listeners.
255     *
256     * @param anchor  the anchor (<code>null</code> not permitted).
257     *
258     * @see #getFrameAnchor()
259     */
260    public void setFrameAnchor(RectangleAnchor anchor) {
261        if (anchor == null) {
262            throw new IllegalArgumentException("Null 'anchor' argument.");
263        }
264        this.frameAnchor = anchor;
265        notifyListeners(new DialLayerChangeEvent(this));
266    }
267
268    /**
269     * Returns the template value.
270     *
271     * @return The template value (never <code>null</code>).
272     *
273     * @see #setTemplateValue(Number)
274     */
275    public Number getTemplateValue() {
276        return this.templateValue;
277    }
278
279    /**
280     * Sets the template value and sends a {@link DialLayerChangeEvent} to
281     * all registered listeners.
282     *
283     * @param value  the value (<code>null</code> not permitted).
284     *
285     * @see #setTemplateValue(Number)
286     */
287    public void setTemplateValue(Number value) {
288        if (value == null) {
289            throw new IllegalArgumentException("Null 'value' argument.");
290        }
291        this.templateValue = value;
292        notifyListeners(new DialLayerChangeEvent(this));
293    }
294
295    /**
296     * Returns the template value for the maximum size of the indicator
297     * bounds.
298     *
299     * @return The template value (possibly <code>null</code>).
300     *
301     * @since 1.0.14
302     *
303     * @see #setMaxTemplateValue(java.lang.Number)
304     */
305    public Number getMaxTemplateValue() {
306        return this.maxTemplateValue;
307    }
308
309    /**
310     * Sets the template value for the maximum size of the indicator bounds
311     * and sends a {@link DialLayerChangeEvent} to all registered listeners.
312     *
313     * @param value  the value (<code>null</code> permitted).
314     *
315     * @since 1.0.14
316     *
317     * @see #getMaxTemplateValue()
318     */
319    public void setMaxTemplateValue(Number value) {
320        this.maxTemplateValue = value;
321        notifyListeners(new DialLayerChangeEvent(this));
322    }
323
324    /**
325     * Returns the formatter used to format the value.
326     *
327     * @return The formatter (never <code>null</code>).
328     *
329     * @see #setNumberFormat(NumberFormat)
330     */
331    public NumberFormat getNumberFormat() {
332        return this.formatter;
333    }
334
335    /**
336     * Sets the formatter used to format the value and sends a
337     * {@link DialLayerChangeEvent} to all registered listeners.
338     *
339     * @param formatter  the formatter (<code>null</code> not permitted).
340     *
341     * @see #getNumberFormat()
342     */
343    public void setNumberFormat(NumberFormat formatter) {
344        if (formatter == null) {
345            throw new IllegalArgumentException("Null 'formatter' argument.");
346        }
347        this.formatter = formatter;
348        notifyListeners(new DialLayerChangeEvent(this));
349    }
350
351    /**
352     * Returns the font.
353     *
354     * @return The font (never <code>null</code>).
355     *
356     * @see #getFont()
357     */
358    public Font getFont() {
359        return this.font;
360    }
361
362    /**
363     * Sets the font and sends a {@link DialLayerChangeEvent} to all registered
364     * listeners.
365     *
366     * @param font  the font (<code>null</code> not permitted).
367     */
368    public void setFont(Font font) {
369        if (font == null) {
370            throw new IllegalArgumentException("Null 'font' argument.");
371        }
372        this.font = font;
373        notifyListeners(new DialLayerChangeEvent(this));
374    }
375
376    /**
377     * Returns the paint.
378     *
379     * @return The paint (never <code>null</code>).
380     *
381     * @see #setPaint(Paint)
382     */
383    public Paint getPaint() {
384        return this.paint;
385    }
386
387    /**
388     * Sets the paint and sends a {@link DialLayerChangeEvent} to all
389     * registered listeners.
390     *
391     * @param paint  the paint (<code>null</code> not permitted).
392     *
393     * @see #getPaint()
394     */
395    public void setPaint(Paint paint) {
396        if (paint == null) {
397            throw new IllegalArgumentException("Null 'paint' argument.");
398        }
399        this.paint = paint;
400        notifyListeners(new DialLayerChangeEvent(this));
401    }
402
403    /**
404     * Returns the background paint.
405     *
406     * @return The background paint.
407     *
408     * @see #setBackgroundPaint(Paint)
409     */
410    public Paint getBackgroundPaint() {
411        return this.backgroundPaint;
412    }
413
414    /**
415     * Sets the background paint and sends a {@link DialLayerChangeEvent} to
416     * all registered listeners.
417     *
418     * @param paint  the paint (<code>null</code> not permitted).
419     *
420     * @see #getBackgroundPaint()
421     */
422    public void setBackgroundPaint(Paint paint) {
423        if (paint == null) {
424            throw new IllegalArgumentException("Null 'paint' argument.");
425        }
426        this.backgroundPaint = paint;
427        notifyListeners(new DialLayerChangeEvent(this));
428    }
429
430    /**
431     * Returns the outline stroke.
432     *
433     * @return The outline stroke (never <code>null</code>).
434     *
435     * @see #setOutlineStroke(Stroke)
436     */
437    public Stroke getOutlineStroke() {
438        return this.outlineStroke;
439    }
440
441    /**
442     * Sets the outline stroke and sends a {@link DialLayerChangeEvent} to
443     * all registered listeners.
444     *
445     * @param stroke  the stroke (<code>null</code> not permitted).
446     *
447     * @see #getOutlineStroke()
448     */
449    public void setOutlineStroke(Stroke stroke) {
450        if (stroke == null) {
451            throw new IllegalArgumentException("Null 'stroke' argument.");
452        }
453        this.outlineStroke = stroke;
454        notifyListeners(new DialLayerChangeEvent(this));
455    }
456
457    /**
458     * Returns the outline paint.
459     *
460     * @return The outline paint (never <code>null</code>).
461     *
462     * @see #setOutlinePaint(Paint)
463     */
464    public Paint getOutlinePaint() {
465        return this.outlinePaint;
466    }
467
468    /**
469     * Sets the outline paint and sends a {@link DialLayerChangeEvent} to all
470     * registered listeners.
471     *
472     * @param paint  the paint (<code>null</code> not permitted).
473     *
474     * @see #getOutlinePaint()
475     */
476    public void setOutlinePaint(Paint paint) {
477        if (paint == null) {
478            throw new IllegalArgumentException("Null 'paint' argument.");
479        }
480        this.outlinePaint = paint;
481        notifyListeners(new DialLayerChangeEvent(this));
482    }
483
484    /**
485     * Returns the insets.
486     *
487     * @return The insets (never <code>null</code>).
488     *
489     * @see #setInsets(RectangleInsets)
490     */
491    public RectangleInsets getInsets() {
492        return this.insets;
493    }
494
495    /**
496     * Sets the insets and sends a {@link DialLayerChangeEvent} to all
497     * registered listeners.
498     *
499     * @param insets  the insets (<code>null</code> not permitted).
500     *
501     * @see #getInsets()
502     */
503    public void setInsets(RectangleInsets insets) {
504        if (insets == null) {
505            throw new IllegalArgumentException("Null 'insets' argument.");
506        }
507        this.insets = insets;
508        notifyListeners(new DialLayerChangeEvent(this));
509    }
510
511    /**
512     * Returns the value anchor.
513     *
514     * @return The value anchor (never <code>null</code>).
515     *
516     * @see #setValueAnchor(RectangleAnchor)
517     */
518    public RectangleAnchor getValueAnchor() {
519        return this.valueAnchor;
520    }
521
522    /**
523     * Sets the value anchor and sends a {@link DialLayerChangeEvent} to all
524     * registered listeners.
525     *
526     * @param anchor  the anchor (<code>null</code> not permitted).
527     *
528     * @see #getValueAnchor()
529     */
530    public void setValueAnchor(RectangleAnchor anchor) {
531        if (anchor == null) {
532            throw new IllegalArgumentException("Null 'anchor' argument.");
533        }
534        this.valueAnchor = anchor;
535        notifyListeners(new DialLayerChangeEvent(this));
536    }
537
538    /**
539     * Returns the text anchor.
540     *
541     * @return The text anchor (never <code>null</code>).
542     *
543     * @see #setTextAnchor(TextAnchor)
544     */
545    public TextAnchor getTextAnchor() {
546        return this.textAnchor;
547    }
548
549    /**
550     * Sets the text anchor and sends a {@link DialLayerChangeEvent} to all
551     * registered listeners.
552     *
553     * @param anchor  the anchor (<code>null</code> not permitted).
554     *
555     * @see #getTextAnchor()
556     */
557    public void setTextAnchor(TextAnchor anchor) {
558        if (anchor == null) {
559            throw new IllegalArgumentException("Null 'anchor' argument.");
560        }
561        this.textAnchor = anchor;
562        notifyListeners(new DialLayerChangeEvent(this));
563    }
564
565    /**
566     * Returns <code>true</code> to indicate that this layer should be
567     * clipped within the dial window.
568     *
569     * @return <code>true</code>.
570     */
571    public boolean isClippedToWindow() {
572        return true;
573    }
574
575    /**
576     * Draws the background to the specified graphics device.  If the dial
577     * frame specifies a window, the clipping region will already have been
578     * set to this window before this method is called.
579     *
580     * @param g2  the graphics device (<code>null</code> not permitted).
581     * @param plot  the plot (ignored here).
582     * @param frame  the dial frame (ignored here).
583     * @param view  the view rectangle (<code>null</code> not permitted).
584     */
585    public void draw(Graphics2D g2, DialPlot plot, Rectangle2D frame,
586            Rectangle2D view) {
587
588        // work out the anchor point
589        Rectangle2D f = DialPlot.rectangleByRadius(frame, this.radius,
590                this.radius);
591        Arc2D arc = new Arc2D.Double(f, this.angle, 0.0, Arc2D.OPEN);
592        Point2D pt = arc.getStartPoint();
593
594        // the indicator bounds is calculated from the templateValue (which
595        // determines the minimum size), the maxTemplateValue (which, if
596        // specified, provides a maximum size) and the actual value
597        FontMetrics fm = g2.getFontMetrics(this.font);
598        double value = plot.getValue(this.datasetIndex);
599        String valueStr = this.formatter.format(value);
600        Rectangle2D valueBounds = TextUtilities.getTextBounds(valueStr, g2, fm);
601
602        // calculate the bounds of the template value
603        String s = this.formatter.format(this.templateValue);
604        Rectangle2D tb = TextUtilities.getTextBounds(s, g2, fm);
605        double minW = tb.getWidth();
606        double minH = tb.getHeight();
607
608        double maxW = Double.MAX_VALUE;
609        double maxH = Double.MAX_VALUE;
610        if (this.maxTemplateValue != null) {
611            s = this.formatter.format(this.maxTemplateValue);
612            tb = TextUtilities.getTextBounds(s, g2, fm);
613            maxW = Math.max(tb.getWidth(), minW);
614            maxH = Math.max(tb.getHeight(), minH);
615        }
616        double w = fixToRange(valueBounds.getWidth(), minW, maxW);
617        double h = fixToRange(valueBounds.getHeight(), minH, maxH);
618
619        // align this rectangle to the frameAnchor
620        Rectangle2D bounds = RectangleAnchor.createRectangle(new Size2D(w, h),
621                pt.getX(), pt.getY(), this.frameAnchor);
622
623        // add the insets
624        Rectangle2D fb = this.insets.createOutsetRectangle(bounds);
625
626        // draw the background
627        g2.setPaint(this.backgroundPaint);
628        g2.fill(fb);
629
630        // draw the border
631        g2.setStroke(this.outlineStroke);
632        g2.setPaint(this.outlinePaint);
633        g2.draw(fb);
634
635        // now find the text anchor point
636        Shape savedClip = g2.getClip();
637        g2.clip(fb);
638
639        Point2D pt2 = RectangleAnchor.coordinates(bounds, this.valueAnchor);
640        g2.setPaint(this.paint);
641        g2.setFont(this.font);
642        TextUtilities.drawAlignedString(valueStr, g2, (float) pt2.getX(),
643                (float) pt2.getY(), this.textAnchor);
644        g2.setClip(savedClip);
645
646    }
647
648    /**
649     * A utility method that adjusts a value, if necessary, to be within a 
650     * specified range.
651     * 
652     * @param x  the value.
653     * @param minX  the minimum value in the range.
654     * @param maxX  the maximum value in the range.
655     * 
656     * @return The adjusted value.
657     */
658    private double fixToRange(double x, double minX, double maxX) {
659        if (minX > maxX) {
660            throw new IllegalArgumentException("Requires 'minX' <= 'maxX'.");
661        }
662        if (x < minX) {
663            return minX;
664        }
665        else if (x > maxX) {
666            return maxX;
667        }
668        else {
669            return x;
670        }
671    }
672
673    /**
674     * Tests this instance for equality with an arbitrary object.
675     *
676     * @param obj  the object (<code>null</code> permitted).
677     *
678     * @return A boolean.
679     */
680    public boolean equals(Object obj) {
681        if (obj == this) {
682            return true;
683        }
684        if (!(obj instanceof DialValueIndicator)) {
685            return false;
686        }
687        DialValueIndicator that = (DialValueIndicator) obj;
688        if (this.datasetIndex != that.datasetIndex) {
689            return false;
690        }
691        if (this.angle != that.angle) {
692            return false;
693        }
694        if (this.radius != that.radius) {
695            return false;
696        }
697        if (!this.frameAnchor.equals(that.frameAnchor)) {
698            return false;
699        }
700        if (!this.templateValue.equals(that.templateValue)) {
701            return false;
702        }
703        if (!ObjectUtilities.equal(this.maxTemplateValue,
704                that.maxTemplateValue)) {
705            return false;
706        }
707        if (!this.font.equals(that.font)) {
708            return false;
709        }
710        if (!PaintUtilities.equal(this.paint, that.paint)) {
711            return false;
712        }
713        if (!PaintUtilities.equal(this.backgroundPaint, that.backgroundPaint)) {
714            return false;
715        }
716        if (!this.outlineStroke.equals(that.outlineStroke)) {
717            return false;
718        }
719        if (!PaintUtilities.equal(this.outlinePaint, that.outlinePaint)) {
720            return false;
721        }
722        if (!this.insets.equals(that.insets)) {
723            return false;
724        }
725        if (!this.valueAnchor.equals(that.valueAnchor)) {
726            return false;
727        }
728        if (!this.textAnchor.equals(that.textAnchor)) {
729            return false;
730        }
731        return super.equals(obj);
732    }
733
734    /**
735     * Returns a hash code for this instance.
736     *
737     * @return The hash code.
738     */
739    public int hashCode() {
740        int result = 193;
741        result = 37 * result + HashUtilities.hashCodeForPaint(this.paint);
742        result = 37 * result + HashUtilities.hashCodeForPaint(
743                this.backgroundPaint);
744        result = 37 * result + HashUtilities.hashCodeForPaint(
745                this.outlinePaint);
746        result = 37 * result + this.outlineStroke.hashCode();
747        return result;
748    }
749
750    /**
751     * Returns a clone of this instance.
752     *
753     * @return The clone.
754     *
755     * @throws CloneNotSupportedException if some attribute of this instance
756     *     cannot be cloned.
757     */
758    public Object clone() throws CloneNotSupportedException {
759        return super.clone();
760    }
761
762    /**
763     * Provides serialization support.
764     *
765     * @param stream  the output stream.
766     *
767     * @throws IOException  if there is an I/O error.
768     */
769    private void writeObject(ObjectOutputStream stream) throws IOException {
770        stream.defaultWriteObject();
771        SerialUtilities.writePaint(this.paint, stream);
772        SerialUtilities.writePaint(this.backgroundPaint, stream);
773        SerialUtilities.writePaint(this.outlinePaint, stream);
774        SerialUtilities.writeStroke(this.outlineStroke, stream);
775    }
776
777    /**
778     * Provides serialization support.
779     *
780     * @param stream  the input stream.
781     *
782     * @throws IOException  if there is an I/O error.
783     * @throws ClassNotFoundException  if there is a classpath problem.
784     */
785    private void readObject(ObjectInputStream stream)
786            throws IOException, ClassNotFoundException {
787        stream.defaultReadObject();
788        this.paint = SerialUtilities.readPaint(stream);
789        this.backgroundPaint = SerialUtilities.readPaint(stream);
790        this.outlinePaint = SerialUtilities.readPaint(stream);
791        this.outlineStroke = SerialUtilities.readStroke(stream);
792    }
793
794}