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