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