001/* ===========================================================
002 * JFreeChart : a free chart library for the Java(tm) platform
003 * ===========================================================
004 *
005 * (C) Copyright 2000-2022, 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-2022, 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.internal.HashUtils;
054import org.jfree.chart.internal.PaintUtils;
055import org.jfree.chart.internal.Args;
056import org.jfree.chart.api.PublicCloneable;
057import org.jfree.chart.internal.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        /** The fill paint. */
394        private transient Paint fillPaint;
395
396        /** The outline paint. */
397        private transient Paint outlinePaint;
398
399        /**
400         * Creates a new instance.
401         */
402        public Pointer() {
403            this(0);
404        }
405
406        /**
407         * Creates a new instance.
408         *
409         * @param datasetIndex  the dataset index.
410         */
411        public Pointer(int datasetIndex) {
412            super(datasetIndex);
413            this.widthRadius = 0.05;
414            this.fillPaint = Color.GRAY;
415            this.outlinePaint = Color.BLACK;
416        }
417
418        /**
419         * Returns the width radius.
420         *
421         * @return The width radius.
422         *
423         * @see #setWidthRadius(double)
424         */
425        public double getWidthRadius() {
426            return this.widthRadius;
427        }
428
429        /**
430         * Sets the width radius and sends a {@link DialLayerChangeEvent} to
431         * all registered listeners.
432         *
433         * @param radius  the radius
434         *
435         * @see #getWidthRadius()
436         */
437        public void setWidthRadius(double radius) {
438            this.widthRadius = radius;
439            notifyListeners(new DialLayerChangeEvent(this));
440        }
441
442        /**
443         * Returns the fill paint.
444         *
445         * @return The paint (never {@code null}).
446         *
447         * @see #setFillPaint(Paint)
448         */
449        public Paint getFillPaint() {
450            return this.fillPaint;
451        }
452
453        /**
454         * Sets the fill paint and sends a {@link DialLayerChangeEvent} to all
455         * registered listeners.
456         *
457         * @param paint  the paint ({@code null} not permitted).
458         *
459         * @see #getFillPaint()
460         */
461        public void setFillPaint(Paint paint) {
462            Args.nullNotPermitted(paint, "paint");
463            this.fillPaint = paint;
464            notifyListeners(new DialLayerChangeEvent(this));
465        }
466
467        /**
468         * Returns the outline paint.
469         *
470         * @return The paint (never {@code null}).
471         *
472         * @see #setOutlinePaint(Paint)
473         */
474        public Paint getOutlinePaint() {
475            return this.outlinePaint;
476        }
477
478        /**
479         * Sets the outline paint and sends a {@link DialLayerChangeEvent} to
480         * all registered listeners.
481         *
482         * @param paint  the paint ({@code null} not permitted).
483         *
484         * @see #getOutlinePaint()
485         */
486        public void setOutlinePaint(Paint paint) {
487            Args.nullNotPermitted(paint, "paint");
488            this.outlinePaint = paint;
489            notifyListeners(new DialLayerChangeEvent(this));
490        }
491
492        /**
493         * Draws the pointer.
494         *
495         * @param g2  the graphics target.
496         * @param plot  the plot.
497         * @param frame  the dial's reference frame.
498         * @param view  the dial's view.
499         */
500        @Override
501        public void draw(Graphics2D g2, DialPlot plot, Rectangle2D frame,
502                Rectangle2D view) {
503
504            g2.setPaint(Color.BLUE);
505            g2.setStroke(new BasicStroke(1.0f));
506            Rectangle2D lengthRect = DialPlot.rectangleByRadius(frame,
507                    this.radius, this.radius);
508            Rectangle2D widthRect = DialPlot.rectangleByRadius(frame,
509                    this.widthRadius, this.widthRadius);
510            double value = plot.getValue(this.datasetIndex);
511            DialScale scale = plot.getScaleForDataset(this.datasetIndex);
512            double angle = scale.valueToAngle(value);
513
514            Arc2D arc1 = new Arc2D.Double(lengthRect, angle, 0, Arc2D.OPEN);
515            Point2D pt1 = arc1.getEndPoint();
516            Arc2D arc2 = new Arc2D.Double(widthRect, angle - 90.0, 180.0,
517                    Arc2D.OPEN);
518            Point2D pt2 = arc2.getStartPoint();
519            Point2D pt3 = arc2.getEndPoint();
520            Arc2D arc3 = new Arc2D.Double(widthRect, angle - 180.0, 0.0,
521                    Arc2D.OPEN);
522            Point2D pt4 = arc3.getStartPoint();
523
524            GeneralPath gp = new GeneralPath();
525            gp.moveTo((float) pt1.getX(), (float) pt1.getY());
526            gp.lineTo((float) pt2.getX(), (float) pt2.getY());
527            gp.lineTo((float) pt4.getX(), (float) pt4.getY());
528            gp.lineTo((float) pt3.getX(), (float) pt3.getY());
529            gp.closePath();
530            g2.setPaint(this.fillPaint);
531            g2.fill(gp);
532
533            g2.setPaint(this.outlinePaint);
534            Line2D line = new Line2D.Double(frame.getCenterX(),
535                    frame.getCenterY(), pt1.getX(), pt1.getY());
536            g2.draw(line);
537
538            line.setLine(pt2, pt3);
539            g2.draw(line);
540
541            line.setLine(pt3, pt1);
542            g2.draw(line);
543
544            line.setLine(pt2, pt1);
545            g2.draw(line);
546
547            line.setLine(pt2, pt4);
548            g2.draw(line);
549
550            line.setLine(pt3, pt4);
551            g2.draw(line);
552        }
553
554        /**
555         * Tests this pointer for equality with an arbitrary object.
556         *
557         * @param obj  the object ({@code null} permitted).
558         *
559         * @return A boolean.
560         */
561        @Override
562        public boolean equals(Object obj) {
563            if (obj == this) {
564                return true;
565            }
566            if (!(obj instanceof DialPointer.Pointer)) {
567                return false;
568            }
569            DialPointer.Pointer that = (DialPointer.Pointer) obj;
570
571            if (this.widthRadius != that.widthRadius) {
572                return false;
573            }
574            if (!PaintUtils.equal(this.fillPaint, that.fillPaint)) {
575                return false;
576            }
577            if (!PaintUtils.equal(this.outlinePaint, that.outlinePaint)) {
578                return false;
579            }
580            return super.equals(obj);
581        }
582
583        /**
584         * Returns a hash code for this instance.
585         *
586         * @return A hash code.
587         */
588        @Override
589        public int hashCode() {
590            int result = super.hashCode();
591            result = HashUtils.hashCode(result, this.widthRadius);
592            result = HashUtils.hashCode(result, this.fillPaint);
593            result = HashUtils.hashCode(result, this.outlinePaint);
594            return result;
595        }
596
597        /**
598         * Provides serialization support.
599         *
600         * @param stream  the output stream.
601         *
602         * @throws IOException  if there is an I/O error.
603         */
604        private void writeObject(ObjectOutputStream stream) throws IOException {
605            stream.defaultWriteObject();
606            SerialUtils.writePaint(this.fillPaint, stream);
607            SerialUtils.writePaint(this.outlinePaint, stream);
608        }
609
610        /**
611         * Provides serialization support.
612         *
613         * @param stream  the input stream.
614         *
615         * @throws IOException  if there is an I/O error.
616         * @throws ClassNotFoundException  if there is a classpath problem.
617         */
618        private void readObject(ObjectInputStream stream)
619                throws IOException, ClassNotFoundException {
620            stream.defaultReadObject();
621            this.fillPaint = SerialUtils.readPaint(stream);
622            this.outlinePaint = SerialUtils.readPaint(stream);
623        }
624
625    }
626
627}