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