001/* ===========================================================
002 * JFreeChart : a free chart library for the Java(tm) platform
003 * ===========================================================
004 *
005 * (C) Copyright 2000-2017, 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 * DialPointer.java
029 * ----------------
030 * (C) Copyright 2006-2017, 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 : Added equals() overrides (DG);
039 * 24-Oct-2007 : Implemented PublicCloneable, changed default radius,
040 *               and added argument checks (DG);
041 * 23-Nov-2007 : Added fillPaint and outlinePaint attributes to
042 *               DialPointer.Pointer (DG);
043 * 03-Jul-2013 : Use ParamChecks (DG);
044 *
045 */
046
047package org.jfree.chart.plot.dial;
048
049import java.awt.BasicStroke;
050import java.awt.Color;
051import java.awt.Graphics2D;
052import java.awt.Paint;
053import java.awt.Stroke;
054import java.awt.geom.Arc2D;
055import java.awt.geom.GeneralPath;
056import java.awt.geom.Line2D;
057import java.awt.geom.Point2D;
058import java.awt.geom.Rectangle2D;
059import java.io.IOException;
060import java.io.ObjectInputStream;
061import java.io.ObjectOutputStream;
062import java.io.Serializable;
063import org.jfree.chart.HashUtils;
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 base class for the pointer in a {@link DialPlot}.
071 *
072 * @since 1.0.7
073 */
074public abstract class DialPointer extends AbstractDialLayer
075        implements DialLayer, Cloneable, PublicCloneable, Serializable {
076
077    /** The needle radius. */
078    double radius;
079
080    /**
081     * The dataset index for the needle.
082     */
083    int datasetIndex;
084
085    /**
086     * Creates a new {@code DialPointer} instance.
087     */
088    protected DialPointer() {
089        this(0);
090    }
091
092    /**
093     * Creates a new pointer for the specified dataset.
094     *
095     * @param datasetIndex  the dataset index.
096     */
097    protected DialPointer(int datasetIndex) {
098        this.radius = 0.9;
099        this.datasetIndex = datasetIndex;
100    }
101
102    /**
103     * Returns the dataset index that the pointer maps to.
104     *
105     * @return The dataset index.
106     *
107     * @see #getDatasetIndex()
108     */
109    public int getDatasetIndex() {
110        return this.datasetIndex;
111    }
112
113    /**
114     * Sets the dataset index for the pointer and sends a
115     * {@link DialLayerChangeEvent} to all registered listeners.
116     *
117     * @param index  the index.
118     *
119     * @see #getDatasetIndex()
120     */
121    public void setDatasetIndex(int index) {
122        this.datasetIndex = index;
123        notifyListeners(new DialLayerChangeEvent(this));
124    }
125
126    /**
127     * Returns the radius of the pointer, as a percentage of the dial's
128     * framing rectangle.
129     *
130     * @return The radius.
131     *
132     * @see #setRadius(double)
133     */
134    public double getRadius() {
135        return this.radius;
136    }
137
138    /**
139     * Sets the radius of the pointer and sends a
140     * {@link DialLayerChangeEvent} to all registered listeners.
141     *
142     * @param radius  the radius.
143     *
144     * @see #getRadius()
145     */
146    public void setRadius(double radius) {
147        this.radius = radius;
148        notifyListeners(new DialLayerChangeEvent(this));
149    }
150
151    /**
152     * Returns {@code true} to indicate that this layer should be
153     * clipped within the dial window.
154     *
155     * @return {@code true}.
156     */
157    @Override
158    public boolean isClippedToWindow() {
159        return true;
160    }
161
162    /**
163     * Checks this instance for equality with an arbitrary object.
164     *
165     * @param obj  the object ({@code null} not permitted).
166     *
167     * @return A boolean.
168     */
169    @Override
170    public boolean equals(Object obj) {
171        if (obj == this) {
172            return true;
173        }
174        if (!(obj instanceof DialPointer)) {
175            return false;
176        }
177        DialPointer that = (DialPointer) obj;
178        if (this.datasetIndex != that.datasetIndex) {
179            return false;
180        }
181        if (this.radius != that.radius) {
182            return false;
183        }
184        return super.equals(obj);
185    }
186
187    /**
188     * Returns a hash code.
189     *
190     * @return A hash code.
191     */
192    @Override
193    public int hashCode() {
194        int result = 23;
195        result = HashUtils.hashCode(result, this.radius);
196        return result;
197    }
198
199    /**
200     * Returns a clone of the pointer.
201     *
202     * @return a clone.
203     *
204     * @throws CloneNotSupportedException if one of the attributes cannot
205     *     be cloned.
206     */
207    @Override
208    public Object clone() throws CloneNotSupportedException {
209        return super.clone();
210    }
211
212    /**
213     * A dial pointer that draws a thin line (like a pin).
214     */
215    public static class Pin extends DialPointer {
216
217        /** For serialization. */
218        static final long serialVersionUID = -8445860485367689750L;
219
220        /** The paint. */
221        private transient Paint paint;
222
223        /** The stroke. */
224        private transient Stroke stroke;
225
226        /**
227         * Creates a new instance.
228         */
229        public Pin() {
230            this(0);
231        }
232
233        /**
234         * Creates a new instance.
235         *
236         * @param datasetIndex  the dataset index.
237         */
238        public Pin(int datasetIndex) {
239            super(datasetIndex);
240            this.paint = Color.RED;
241            this.stroke = new BasicStroke(3.0f, BasicStroke.CAP_ROUND,
242                    BasicStroke.JOIN_BEVEL);
243        }
244
245        /**
246         * Returns the paint.
247         *
248         * @return The paint (never {@code null}).
249         *
250         * @see #setPaint(Paint)
251         */
252        public Paint getPaint() {
253            return this.paint;
254        }
255
256        /**
257         * Sets the paint and sends a {@link DialLayerChangeEvent} to all
258         * registered listeners.
259         *
260         * @param paint  the paint ({@code null} not permitted).
261         *
262         * @see #getPaint()
263         */
264        public void setPaint(Paint paint) {
265            Args.nullNotPermitted(paint, "paint");
266            this.paint = paint;
267            notifyListeners(new DialLayerChangeEvent(this));
268        }
269
270        /**
271         * Returns the stroke.
272         *
273         * @return The stroke (never {@code null}).
274         *
275         * @see #setStroke(Stroke)
276         */
277        public Stroke getStroke() {
278            return this.stroke;
279        }
280
281        /**
282         * Sets the stroke and sends a {@link DialLayerChangeEvent} to all
283         * registered listeners.
284         *
285         * @param stroke  the stroke ({@code null} not permitted).
286         *
287         * @see #getStroke()
288         */
289        public void setStroke(Stroke stroke) {
290            Args.nullNotPermitted(stroke, "stroke");
291            this.stroke = stroke;
292            notifyListeners(new DialLayerChangeEvent(this));
293        }
294
295        /**
296         * Draws the pointer.
297         *
298         * @param g2  the graphics target.
299         * @param plot  the plot.
300         * @param frame  the dial's reference frame.
301         * @param view  the dial's view.
302         */
303        @Override
304        public void draw(Graphics2D g2, DialPlot plot, Rectangle2D frame,
305            Rectangle2D view) {
306
307            g2.setPaint(this.paint);
308            g2.setStroke(this.stroke);
309            Rectangle2D arcRect = DialPlot.rectangleByRadius(frame,
310                    this.radius, this.radius);
311
312            double value = plot.getValue(this.datasetIndex);
313            DialScale scale = plot.getScaleForDataset(this.datasetIndex);
314            double angle = scale.valueToAngle(value);
315
316            Arc2D arc = new Arc2D.Double(arcRect, angle, 0, Arc2D.OPEN);
317            Point2D pt = arc.getEndPoint();
318
319            Line2D line = new Line2D.Double(frame.getCenterX(),
320                    frame.getCenterY(), pt.getX(), pt.getY());
321            g2.draw(line);
322        }
323
324        /**
325         * Tests this pointer for equality with an arbitrary object.
326         *
327         * @param obj  the object ({@code null} permitted).
328         *
329         * @return A boolean.
330         */
331        @Override
332        public boolean equals(Object obj) {
333            if (obj == this) {
334                return true;
335            }
336            if (!(obj instanceof DialPointer.Pin)) {
337                return false;
338            }
339            DialPointer.Pin that = (DialPointer.Pin) obj;
340            if (!PaintUtils.equal(this.paint, that.paint)) {
341                return false;
342            }
343            if (!this.stroke.equals(that.stroke)) {
344                return false;
345            }
346            return super.equals(obj);
347        }
348
349        /**
350         * Returns a hash code for this instance.
351         *
352         * @return A hash code.
353         */
354        @Override
355        public int hashCode() {
356            int result = super.hashCode();
357            result = HashUtils.hashCode(result, this.paint);
358            result = HashUtils.hashCode(result, this.stroke);
359            return result;
360        }
361
362        /**
363         * Provides serialization support.
364         *
365         * @param stream  the output stream.
366         *
367         * @throws IOException  if there is an I/O error.
368         */
369        private void writeObject(ObjectOutputStream stream) throws IOException {
370            stream.defaultWriteObject();
371            SerialUtils.writePaint(this.paint, stream);
372            SerialUtils.writeStroke(this.stroke, stream);
373        }
374
375        /**
376         * Provides serialization support.
377         *
378         * @param stream  the input stream.
379         *
380         * @throws IOException  if there is an I/O error.
381         * @throws ClassNotFoundException  if there is a classpath problem.
382         */
383        private void readObject(ObjectInputStream stream)
384                throws IOException, ClassNotFoundException {
385            stream.defaultReadObject();
386            this.paint = SerialUtils.readPaint(stream);
387            this.stroke = SerialUtils.readStroke(stream);
388        }
389
390    }
391
392    /**
393     * A dial pointer.
394     */
395    public static class Pointer extends DialPointer {
396
397        /** For serialization. */
398        static final long serialVersionUID = -4180500011963176960L;
399
400        /**
401         * The radius that defines the width of the pointer at the base.
402         */
403        private double widthRadius;
404
405        /**
406         * The fill paint.
407         *
408         * @since 1.0.8
409         */
410        private transient Paint fillPaint;
411
412        /**
413         * The outline paint.
414         *
415         * @since 1.0.8
416         */
417        private transient Paint outlinePaint;
418
419        /**
420         * Creates a new instance.
421         */
422        public Pointer() {
423            this(0);
424        }
425
426        /**
427         * Creates a new instance.
428         *
429         * @param datasetIndex  the dataset index.
430         */
431        public Pointer(int datasetIndex) {
432            super(datasetIndex);
433            this.widthRadius = 0.05;
434            this.fillPaint = Color.GRAY;
435            this.outlinePaint = Color.BLACK;
436        }
437
438        /**
439         * Returns the width radius.
440         *
441         * @return The width radius.
442         *
443         * @see #setWidthRadius(double)
444         */
445        public double getWidthRadius() {
446            return this.widthRadius;
447        }
448
449        /**
450         * Sets the width radius and sends a {@link DialLayerChangeEvent} to
451         * all registered listeners.
452         *
453         * @param radius  the radius
454         *
455         * @see #getWidthRadius()
456         */
457        public void setWidthRadius(double radius) {
458            this.widthRadius = radius;
459            notifyListeners(new DialLayerChangeEvent(this));
460        }
461
462        /**
463         * Returns the fill paint.
464         *
465         * @return The paint (never {@code null}).
466         *
467         * @see #setFillPaint(Paint)
468         *
469         * @since 1.0.8
470         */
471        public Paint getFillPaint() {
472            return this.fillPaint;
473        }
474
475        /**
476         * Sets the fill paint and sends a {@link DialLayerChangeEvent} to all
477         * registered listeners.
478         *
479         * @param paint  the paint ({@code null} not permitted).
480         *
481         * @see #getFillPaint()
482         *
483         * @since 1.0.8
484         */
485        public void setFillPaint(Paint paint) {
486            Args.nullNotPermitted(paint, "paint");
487            this.fillPaint = paint;
488            notifyListeners(new DialLayerChangeEvent(this));
489        }
490
491        /**
492         * Returns the outline paint.
493         *
494         * @return The paint (never {@code null}).
495         *
496         * @see #setOutlinePaint(Paint)
497         *
498         * @since 1.0.8
499         */
500        public Paint getOutlinePaint() {
501            return this.outlinePaint;
502        }
503
504        /**
505         * Sets the outline paint and sends a {@link DialLayerChangeEvent} to
506         * all registered listeners.
507         *
508         * @param paint  the paint ({@code null} not permitted).
509         *
510         * @see #getOutlinePaint()
511         *
512         * @since 1.0.8
513         */
514        public void setOutlinePaint(Paint paint) {
515            Args.nullNotPermitted(paint, "paint");
516            this.outlinePaint = paint;
517            notifyListeners(new DialLayerChangeEvent(this));
518        }
519
520        /**
521         * Draws the pointer.
522         *
523         * @param g2  the graphics target.
524         * @param plot  the plot.
525         * @param frame  the dial's reference frame.
526         * @param view  the dial's view.
527         */
528        @Override
529        public void draw(Graphics2D g2, DialPlot plot, Rectangle2D frame,
530                Rectangle2D view) {
531
532            g2.setPaint(Color.BLUE);
533            g2.setStroke(new BasicStroke(1.0f));
534            Rectangle2D lengthRect = DialPlot.rectangleByRadius(frame,
535                    this.radius, this.radius);
536            Rectangle2D widthRect = DialPlot.rectangleByRadius(frame,
537                    this.widthRadius, this.widthRadius);
538            double value = plot.getValue(this.datasetIndex);
539            DialScale scale = plot.getScaleForDataset(this.datasetIndex);
540            double angle = scale.valueToAngle(value);
541
542            Arc2D arc1 = new Arc2D.Double(lengthRect, angle, 0, Arc2D.OPEN);
543            Point2D pt1 = arc1.getEndPoint();
544            Arc2D arc2 = new Arc2D.Double(widthRect, angle - 90.0, 180.0,
545                    Arc2D.OPEN);
546            Point2D pt2 = arc2.getStartPoint();
547            Point2D pt3 = arc2.getEndPoint();
548            Arc2D arc3 = new Arc2D.Double(widthRect, angle - 180.0, 0.0,
549                    Arc2D.OPEN);
550            Point2D pt4 = arc3.getStartPoint();
551
552            GeneralPath gp = new GeneralPath();
553            gp.moveTo((float) pt1.getX(), (float) pt1.getY());
554            gp.lineTo((float) pt2.getX(), (float) pt2.getY());
555            gp.lineTo((float) pt4.getX(), (float) pt4.getY());
556            gp.lineTo((float) pt3.getX(), (float) pt3.getY());
557            gp.closePath();
558            g2.setPaint(this.fillPaint);
559            g2.fill(gp);
560
561            g2.setPaint(this.outlinePaint);
562            Line2D line = new Line2D.Double(frame.getCenterX(),
563                    frame.getCenterY(), pt1.getX(), pt1.getY());
564            g2.draw(line);
565
566            line.setLine(pt2, pt3);
567            g2.draw(line);
568
569            line.setLine(pt3, pt1);
570            g2.draw(line);
571
572            line.setLine(pt2, pt1);
573            g2.draw(line);
574
575            line.setLine(pt2, pt4);
576            g2.draw(line);
577
578            line.setLine(pt3, pt4);
579            g2.draw(line);
580        }
581
582        /**
583         * Tests this pointer for equality with an arbitrary object.
584         *
585         * @param obj  the object ({@code null} permitted).
586         *
587         * @return A boolean.
588         */
589        @Override
590        public boolean equals(Object obj) {
591            if (obj == this) {
592                return true;
593            }
594            if (!(obj instanceof DialPointer.Pointer)) {
595                return false;
596            }
597            DialPointer.Pointer that = (DialPointer.Pointer) obj;
598
599            if (this.widthRadius != that.widthRadius) {
600                return false;
601            }
602            if (!PaintUtils.equal(this.fillPaint, that.fillPaint)) {
603                return false;
604            }
605            if (!PaintUtils.equal(this.outlinePaint, that.outlinePaint)) {
606                return false;
607            }
608            return super.equals(obj);
609        }
610
611        /**
612         * Returns a hash code for this instance.
613         *
614         * @return A hash code.
615         */
616        @Override
617        public int hashCode() {
618            int result = super.hashCode();
619            result = HashUtils.hashCode(result, this.widthRadius);
620            result = HashUtils.hashCode(result, this.fillPaint);
621            result = HashUtils.hashCode(result, this.outlinePaint);
622            return result;
623        }
624
625        /**
626         * Provides serialization support.
627         *
628         * @param stream  the output stream.
629         *
630         * @throws IOException  if there is an I/O error.
631         */
632        private void writeObject(ObjectOutputStream stream) throws IOException {
633            stream.defaultWriteObject();
634            SerialUtils.writePaint(this.fillPaint, stream);
635            SerialUtils.writePaint(this.outlinePaint, stream);
636        }
637
638        /**
639         * Provides serialization support.
640         *
641         * @param stream  the input stream.
642         *
643         * @throws IOException  if there is an I/O error.
644         * @throws ClassNotFoundException  if there is a classpath problem.
645         */
646        private void readObject(ObjectInputStream stream)
647                throws IOException, ClassNotFoundException {
648            stream.defaultReadObject();
649            this.fillPaint = SerialUtils.readPaint(stream);
650            this.outlinePaint = SerialUtils.readPaint(stream);
651        }
652
653    }
654
655}