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 * ArcDialFrame.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 * 08-Mar-2007 : Fix in hashCode() (DG);
039 * 17-Oct-2007 : Updated equals() (DG);
040 * 24-Oct-2007 : Added argument checks and API docs, and renamed 
041 *               StandardDialFrame --> ArcDialFrame (DG);
042 * 
043 */
044
045package org.jfree.chart.plot.dial;
046
047import java.awt.BasicStroke;
048import java.awt.Color;
049import java.awt.Graphics2D;
050import java.awt.Paint;
051import java.awt.Shape;
052import java.awt.Stroke;
053import java.awt.geom.Arc2D;
054import java.awt.geom.Area;
055import java.awt.geom.GeneralPath;
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 standard frame for the {@link DialPlot} class.
070 */
071public class ArcDialFrame extends AbstractDialLayer implements DialFrame, 
072        Cloneable, PublicCloneable, Serializable {
073    
074    /** For serialization. */
075    static final long serialVersionUID = -4089176959553523499L;
076
077    /**
078     * The color used for the front of the panel.  This field is transient
079     * because it requires special handling for serialization.
080     */
081    private transient Paint backgroundPaint;
082    
083    /**
084     * The color used for the border around the window. This field is transient
085     * because it requires special handling for serialization.
086     */
087    private transient Paint foregroundPaint;
088    
089    /**
090     * The stroke for drawing the frame outline.  This field is transient
091     * because it requires special handling for serialization.
092     */
093    private transient Stroke stroke;
094       
095    /**
096     * The start angle.
097     */
098    private double startAngle;
099    
100    /**
101     * The end angle.
102     */
103    private double extent;
104    
105    /** The inner radius, relative to the framing rectangle. */
106    private double innerRadius;
107    
108    /** The outer radius, relative to the framing rectangle. */
109    private double outerRadius;
110   
111    /** 
112     * Creates a new instance of <code>ArcDialFrame</code> that spans
113     * 180 degrees. 
114     */
115    public ArcDialFrame() {
116        this(0, 180);
117    }
118    
119    /**
120     * Creates a new instance of <code>ArcDialFrame</code> that spans
121     * the arc specified.
122     *
123     * @param startAngle  the startAngle (in degrees).
124     * @param extent  the extent of the arc (in degrees, counter-clockwise).
125     */
126    public ArcDialFrame(double startAngle, double extent) {
127        this.backgroundPaint = Color.gray;
128        this.foregroundPaint = new Color(100, 100, 150);
129        this.stroke = new BasicStroke(2.0f);
130        this.innerRadius = 0.25;
131        this.outerRadius = 0.75;
132        this.startAngle = startAngle;
133        this.extent = extent;        
134    }
135    
136    /**
137     * Returns the background paint (never <code>null</code>).
138     * 
139     * @return The background paint.
140     * 
141     * @see #setBackgroundPaint(Paint)
142     */
143    public Paint getBackgroundPaint() {
144        return this.backgroundPaint;
145    }
146    
147    /**
148     * Sets the background paint and sends a {@link DialLayerChangeEvent} to 
149     * all registered listeners.
150     * 
151     * @param paint  the paint (<code>null</code> not permitted).
152     * 
153     * @see #getBackgroundPaint()
154     */
155    public void setBackgroundPaint(Paint paint) {
156        if (paint == null) {
157            throw new IllegalArgumentException("Null 'paint' argument.");
158        }
159        this.backgroundPaint = paint;
160        notifyListeners(new DialLayerChangeEvent(this));
161    }
162    
163    /**
164     * Returns the foreground paint.
165     * 
166     * @return The foreground paint (never <code>null</code>).
167     * 
168     * @see #setForegroundPaint(Paint)
169     */
170    public Paint getForegroundPaint() {
171        return this.foregroundPaint;
172    }
173    
174    /**
175     * Sets the foreground paint and sends a {@link DialLayerChangeEvent} to 
176     * all registered listeners.
177     * 
178     * @param paint  the paint (<code>null</code> not permitted).
179     * 
180     * @see #getForegroundPaint()
181     */
182    public void setForegroundPaint(Paint paint) {
183        if (paint == null) {
184            throw new IllegalArgumentException("Null 'paint' argument.");
185        }
186        this.foregroundPaint = paint;
187        notifyListeners(new DialLayerChangeEvent(this));
188    }
189    
190    /**
191     * Returns the stroke.
192     * 
193     * @return The stroke (never <code>null</code>).
194     * 
195     * @see #setStroke(Stroke)
196     */
197    public Stroke getStroke() {
198        return this.stroke;
199    }
200    
201    /**
202     * Sets the stroke and sends a {@link DialLayerChangeEvent} to 
203     * all registered listeners.
204     * 
205     * @param stroke  the stroke (<code>null</code> not permitted).
206     * 
207     * @see #getStroke()
208     */
209    public void setStroke(Stroke stroke) {
210        if (stroke == null) { 
211            throw new IllegalArgumentException("Null 'stroke' argument.");
212        }
213        this.stroke = stroke;
214        notifyListeners(new DialLayerChangeEvent(this));
215    }
216
217    /**
218     * Returns the inner radius, relative to the framing rectangle.
219     *
220     * @return The inner radius. 
221     * 
222     * @see #setInnerRadius(double)
223     */
224    public double getInnerRadius() {
225        return this.innerRadius;
226    }
227    
228    /**
229     * Sets the inner radius and sends a {@link DialLayerChangeEvent} to 
230     * all registered listeners.
231     *
232     * @param radius  the inner radius.
233     * 
234     * @see #getInnerRadius()
235     */
236    public void setInnerRadius(double radius) {
237        if (radius < 0.0) {
238            throw new IllegalArgumentException("Negative 'radius' argument.");
239        }
240        this.innerRadius = radius;
241        notifyListeners(new DialLayerChangeEvent(this));
242    }
243    
244    /**
245     * Returns the outer radius, relative to the framing rectangle.
246     *
247     * @return The outer radius.
248     * 
249     * @see #setOuterRadius(double)
250     */
251    public double getOuterRadius() {
252        return this.outerRadius;
253    }
254    
255    /**
256     * Sets the outer radius and sends a {@link DialLayerChangeEvent} to 
257     * all registered listeners.
258     *
259     * @param radius  the outer radius.
260     * 
261     * @see #getOuterRadius()
262     */
263    public void setOuterRadius(double radius) {
264        if (radius < 0.0) {
265            throw new IllegalArgumentException("Negative 'radius' argument.");
266        }
267        this.outerRadius = radius;
268        notifyListeners(new DialLayerChangeEvent(this));
269    }
270    
271    /**
272     * Returns the start angle.
273     * 
274     * @return The start angle.
275     * 
276     * @see #setStartAngle(double)
277     */
278    public double getStartAngle() {
279        return this.startAngle;
280    }
281    
282    /**
283     * Sets the start angle and sends a {@link DialLayerChangeEvent} to 
284     * all registered listeners.
285     * 
286     * @param angle  the angle.
287     * 
288     * @see #getStartAngle()
289     */
290    public void setStartAngle(double angle) {
291        this.startAngle = angle;
292        notifyListeners(new DialLayerChangeEvent(this));
293    }
294    
295    /**
296     * Returns the extent.
297     * 
298     * @return The extent.
299     * 
300     * @see #setExtent(double)
301     */
302    public double getExtent() {
303        return this.extent;
304    }
305    
306    /**
307     * Sets the extent and sends a {@link DialLayerChangeEvent} to 
308     * all registered listeners.
309     * 
310     * @param extent  the extent.
311     * 
312     * @see #getExtent()
313     */
314    public void setExtent(double extent) {
315        this.extent = extent;
316        notifyListeners(new DialLayerChangeEvent(this));
317    }
318    
319    /**
320     * Returns the shape for the window for this dial.  Some dial layers will
321     * request that their drawing be clipped within this window.
322     *
323     * @param frame  the reference frame (<code>null</code> not permitted).
324     *
325     * @return The shape of the dial's window.
326     */
327    public Shape getWindow(Rectangle2D frame) {
328        
329        Rectangle2D innerFrame = DialPlot.rectangleByRadius(frame, 
330                this.innerRadius, this.innerRadius);
331        Rectangle2D outerFrame = DialPlot.rectangleByRadius(frame, 
332                this.outerRadius, this.outerRadius);
333        Arc2D inner = new Arc2D.Double(innerFrame, this.startAngle, 
334                this.extent, Arc2D.OPEN);
335        Arc2D outer = new Arc2D.Double(outerFrame, this.startAngle 
336                + this.extent, -this.extent, Arc2D.OPEN);
337        GeneralPath p = new GeneralPath();
338        Point2D point1 = inner.getStartPoint();
339        p.moveTo((float) point1.getX(), (float) point1.getY());
340        p.append(inner, true);
341        p.append(outer, true);
342        p.closePath();
343        return p;
344        
345    }
346    
347    /**
348     * Returns the outer window.
349     * 
350     * @param frame  the frame.
351     * 
352     * @return The outer window.
353     */
354    protected Shape getOuterWindow(Rectangle2D frame) {
355        double radiusMargin = 0.02;
356        double angleMargin = 1.5;
357        Rectangle2D innerFrame = DialPlot.rectangleByRadius(frame, 
358                this.innerRadius - radiusMargin, this.innerRadius 
359                - radiusMargin);
360        Rectangle2D outerFrame = DialPlot.rectangleByRadius(frame, 
361                this.outerRadius + radiusMargin, this.outerRadius 
362                + radiusMargin);
363        Arc2D inner = new Arc2D.Double(innerFrame, this.startAngle 
364                - angleMargin, this.extent + 2 * angleMargin, Arc2D.OPEN);
365        Arc2D outer = new Arc2D.Double(outerFrame, this.startAngle 
366                + angleMargin + this.extent, -this.extent - 2 * angleMargin, 
367                Arc2D.OPEN);
368        GeneralPath p = new GeneralPath();
369        Point2D point1 = inner.getStartPoint();
370        p.moveTo((float) point1.getX(), (float) point1.getY());
371        p.append(inner, true);
372        p.append(outer, true);
373        p.closePath();
374        return p;
375    }
376    
377    /**
378     * Draws the frame.
379     * 
380     * @param g2  the graphics target.
381     * @param plot  the plot.
382     * @param frame  the dial's reference frame.
383     * @param view  the dial's view rectangle.
384     */
385    public void draw(Graphics2D g2, DialPlot plot, Rectangle2D frame, 
386            Rectangle2D view) {
387        
388        Shape window = getWindow(frame);
389        Shape outerWindow = getOuterWindow(frame);
390
391        Area area1 = new Area(outerWindow);
392        Area area2 = new Area(window);
393        area1.subtract(area2);
394        g2.setPaint(Color.lightGray);
395        g2.fill(area1);
396        
397        g2.setStroke(this.stroke);
398        g2.setPaint(this.foregroundPaint);
399        g2.draw(window);
400        g2.draw(outerWindow);
401        
402        
403    }
404
405    /**
406     * Returns <code>false</code> to indicate that this dial layer is not
407     * clipped to the dial window.
408     *
409     * @return <code>false</code>.
410     */
411    public boolean isClippedToWindow() {
412        return false;
413    }
414    
415    /**
416     * Tests this instance for equality with an arbitrary object.
417     * 
418     * @param obj  the object (<code>null</code> permitted).
419     * 
420     * @return A boolean.
421     */
422    public boolean equals(Object obj) {
423        if (obj == this) {
424            return true;
425        }
426        if (!(obj instanceof ArcDialFrame)) {
427            return false;
428        }
429        ArcDialFrame that = (ArcDialFrame) obj;
430        if (!PaintUtilities.equal(this.backgroundPaint, that.backgroundPaint)) {
431            return false;
432        }
433        if (!PaintUtilities.equal(this.foregroundPaint, that.foregroundPaint)) {
434            return false;
435        }
436        if (this.startAngle != that.startAngle) {
437            return false;
438        }
439        if (this.extent != that.extent) {
440            return false;
441        }
442        if (this.innerRadius != that.innerRadius) {
443            return false;
444        }
445        if (this.outerRadius != that.outerRadius) {
446            return false;
447        }
448        if (!this.stroke.equals(that.stroke)) {
449            return false;
450        }
451        return super.equals(obj);
452    }
453    
454    /**
455     * Returns a hash code for this instance.
456     * 
457     * @return The hash code.
458     */
459    public int hashCode() {
460        int result = 193;
461        long temp = Double.doubleToLongBits(this.startAngle);
462        result = 37 * result + (int) (temp ^ (temp >>> 32));
463        temp = Double.doubleToLongBits(this.extent);
464        result = 37 * result + (int) (temp ^ (temp >>> 32));
465        temp = Double.doubleToLongBits(this.innerRadius);
466        result = 37 * result + (int) (temp ^ (temp >>> 32));
467        temp = Double.doubleToLongBits(this.outerRadius);
468        result = 37 * result + (int) (temp ^ (temp >>> 32));
469        result = 37 * result + HashUtilities.hashCodeForPaint(
470                this.backgroundPaint);
471        result = 37 * result + HashUtilities.hashCodeForPaint(
472                this.foregroundPaint);
473        result = 37 * result + this.stroke.hashCode();
474        return result;
475    }
476    
477    /**
478     * Returns a clone of this instance.
479     * 
480     * @return A clone.
481     * 
482     * @throws CloneNotSupportedException if any attribute of this instance
483     *     cannot be cloned.
484     */
485    public Object clone() throws CloneNotSupportedException {
486        return super.clone();
487    }
488    
489    /**
490     * Provides serialization support.
491     *
492     * @param stream  the output stream.
493     *
494     * @throws IOException  if there is an I/O error.
495     */
496    private void writeObject(ObjectOutputStream stream) throws IOException {
497        stream.defaultWriteObject();
498        SerialUtilities.writePaint(this.backgroundPaint, stream);
499        SerialUtilities.writePaint(this.foregroundPaint, stream);
500        SerialUtilities.writeStroke(this.stroke, stream);
501    }
502
503    /**
504     * Provides serialization support.
505     *
506     * @param stream  the input stream.
507     *
508     * @throws IOException  if there is an I/O error.
509     * @throws ClassNotFoundException  if there is a classpath problem.
510     */
511    private void readObject(ObjectInputStream stream) 
512            throws IOException, ClassNotFoundException {
513        stream.defaultReadObject();
514        this.backgroundPaint = SerialUtilities.readPaint(stream);
515        this.foregroundPaint = SerialUtilities.readPaint(stream);
516        this.stroke = SerialUtilities.readStroke(stream);
517    }
518    
519}