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