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    
044    package org.jfree.chart.plot.dial;
045    
046    import java.awt.BasicStroke;
047    import java.awt.Color;
048    import java.awt.Font;
049    import java.awt.FontMetrics;
050    import java.awt.Graphics2D;
051    import java.awt.Paint;
052    import java.awt.Shape;
053    import java.awt.Stroke;
054    import java.awt.geom.Arc2D;
055    import java.awt.geom.Point2D;
056    import java.awt.geom.Rectangle2D;
057    import java.io.IOException;
058    import java.io.ObjectInputStream;
059    import java.io.ObjectOutputStream;
060    import java.io.Serializable;
061    import java.text.DecimalFormat;
062    import java.text.NumberFormat;
063    
064    import org.jfree.chart.HashUtilities;
065    import org.jfree.io.SerialUtilities;
066    import org.jfree.text.TextUtilities;
067    import org.jfree.ui.RectangleAnchor;
068    import org.jfree.ui.RectangleInsets;
069    import org.jfree.ui.Size2D;
070    import org.jfree.ui.TextAnchor;
071    import org.jfree.util.ObjectUtilities;
072    import org.jfree.util.PaintUtilities;
073    import org.jfree.util.PublicCloneable;
074    
075    /**
076     * A value indicator for a {@link DialPlot}.
077     *
078     * @since 1.0.7
079     */
080    public 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    }