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