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