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     * DialTextAnnotation.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     * 08-Mar-2007 : Fix in hashCode() (DG);
039     * 17-Oct-2007 : Updated equals() (DG);
040     * 24-Oct-2007 : Added getAnchor() and setAnchor() methods (DG);
041     *
042     */
043    
044    package org.jfree.chart.plot.dial;
045    
046    import java.awt.Color;
047    import java.awt.Font;
048    import java.awt.Graphics2D;
049    import java.awt.Paint;
050    import java.awt.geom.Arc2D;
051    import java.awt.geom.Point2D;
052    import java.awt.geom.Rectangle2D;
053    import java.io.IOException;
054    import java.io.ObjectInputStream;
055    import java.io.ObjectOutputStream;
056    import java.io.Serializable;
057    
058    import org.jfree.chart.HashUtilities;
059    import org.jfree.io.SerialUtilities;
060    import org.jfree.text.TextUtilities;
061    import org.jfree.ui.TextAnchor;
062    import org.jfree.util.PaintUtilities;
063    import org.jfree.util.PublicCloneable;
064    
065    /**
066     * A text annotation for a {@link DialPlot}.
067     *
068     * @since 1.0.7
069     */
070    public class DialTextAnnotation extends AbstractDialLayer implements DialLayer,
071            Cloneable, PublicCloneable, Serializable {
072    
073        /** For serialization. */
074        static final long serialVersionUID = 3065267524054428071L;
075    
076        /** The label text. */
077        private String label;
078    
079        /** The font. */
080        private Font font;
081    
082        /**
083         * The paint for the label.  This field is transient because it requires
084         * special handling for serialization.
085         */
086        private transient Paint paint;
087    
088        /** The angle that defines the anchor point for the annotation. */
089        private double angle;
090    
091        /** The radius that defines the anchor point for the annotation. */
092        private double radius;
093    
094        /** The text anchor to be aligned to the annotation's anchor point. */
095        private TextAnchor anchor;
096    
097        /**
098         * Creates a new instance of <code>DialTextAnnotation</code>.
099         *
100         * @param label  the label (<code>null</code> not permitted).
101         */
102        public DialTextAnnotation(String label) {
103            if (label == null) {
104                throw new IllegalArgumentException("Null 'label' argument.");
105            }
106            this.angle = -90.0;
107            this.radius = 0.3;
108            this.font = new Font("Dialog", Font.BOLD, 14);
109            this.paint = Color.black;
110            this.label = label;
111            this.anchor = TextAnchor.TOP_CENTER;
112        }
113    
114        /**
115         * Returns the label text.
116         *
117         * @return The label text (never <code>null</code).
118         *
119         * @see #setLabel(String)
120         */
121        public String getLabel() {
122            return this.label;
123        }
124    
125        /**
126         * Sets the label and sends a {@link DialLayerChangeEvent} to all
127         * registered listeners.
128         *
129         * @param label  the label (<code>null</code> not permitted).
130         *
131         * @see #getLabel()
132         */
133        public void setLabel(String label) {
134            if (label == null) {
135                throw new IllegalArgumentException("Null 'label' argument.");
136            }
137            this.label = label;
138            notifyListeners(new DialLayerChangeEvent(this));
139        }
140    
141        /**
142         * Returns the font used to display the label.
143         *
144         * @return The font (never <code>null</code>).
145         *
146         * @see #setFont(Font)
147         */
148        public Font getFont() {
149            return this.font;
150        }
151    
152        /**
153         * Sets the font used to display the label and sends a
154         * {@link DialLayerChangeEvent} to all registered listeners.
155         *
156         * @param font  the font (<code>null</code> not permitted).
157         *
158         * @see #getFont()
159         */
160        public void setFont(Font font) {
161            if (font == null) {
162                throw new IllegalArgumentException("Null 'font' argument.");
163            }
164            this.font = font;
165            notifyListeners(new DialLayerChangeEvent(this));
166        }
167    
168        /**
169         * Returns the paint used to display the label.
170         *
171         * @return The paint (never <code>null</code>).
172         *
173         * @see #setPaint(Paint)
174         */
175        public Paint getPaint() {
176            return this.paint;
177        }
178    
179        /**
180         * Sets the paint used to display the label and sends a
181         * {@link DialLayerChangeEvent} to all registered listeners.
182         *
183         * @param paint  the paint (<code>null</code> not permitted).
184         *
185         * @see #getPaint()
186         */
187        public void setPaint(Paint paint) {
188            if (paint == null) {
189                throw new IllegalArgumentException("Null 'paint' argument.");
190            }
191            this.paint = paint;
192            notifyListeners(new DialLayerChangeEvent(this));
193        }
194    
195        /**
196         * Returns the angle used to calculate the anchor point.
197         *
198         * @return The angle (in degrees).
199         *
200         * @see #setAngle(double)
201         * @see #getRadius()
202         */
203        public double getAngle() {
204            return this.angle;
205        }
206    
207        /**
208         * Sets the angle used to calculate the anchor point and sends a
209         * {@link DialLayerChangeEvent} to all registered listeners.
210         *
211         * @param angle  the angle (in degrees).
212         *
213         * @see #getAngle()
214         * @see #setRadius(double)
215         */
216        public void setAngle(double angle) {
217            this.angle = angle;
218            notifyListeners(new DialLayerChangeEvent(this));
219        }
220    
221        /**
222         * Returns the radius used to calculate the anchor point.  This is
223         * specified as a percentage relative to the dial's framing rectangle.
224         *
225         * @return The radius.
226         *
227         * @see #setRadius(double)
228         * @see #getAngle()
229         */
230        public double getRadius() {
231            return this.radius;
232        }
233    
234        /**
235         * Sets the radius used to calculate the anchor point and sends a
236         * {@link DialLayerChangeEvent} to all registered listeners.
237         *
238         * @param radius  the radius (as a percentage of the dial's framing
239         *                rectangle).
240         *
241         * @see #getRadius()
242         * @see #setAngle(double)
243         */
244        public void setRadius(double radius) {
245            if (radius < 0.0) {
246                throw new IllegalArgumentException(
247                        "The 'radius' cannot be negative.");
248            }
249            this.radius = radius;
250            notifyListeners(new DialLayerChangeEvent(this));
251        }
252    
253        /**
254         * Returns the text anchor point that will be aligned to the position
255         * specified by {@link #getAngle()} and {@link #getRadius()}.
256         *
257         * @return The anchor point.
258         *
259         * @see #setAnchor(TextAnchor)
260         */
261        public TextAnchor getAnchor() {
262            return this.anchor;
263        }
264    
265        /**
266         * Sets the text anchor point and sends a {@link DialLayerChangeEvent} to
267         * all registered listeners.
268         *
269         * @param anchor  the anchor point (<code>null</code> not permitted).
270         *
271         * @see #getAnchor()
272         */
273        public void setAnchor(TextAnchor anchor) {
274            if (anchor == null) {
275                throw new IllegalArgumentException("Null 'anchor' argument.");
276            }
277            this.anchor = anchor;
278            notifyListeners(new DialLayerChangeEvent(this));
279        }
280    
281        /**
282         * Returns <code>true</code> to indicate that this layer should be
283         * clipped within the dial window.
284         *
285         * @return <code>true</code>.
286         */
287        public boolean isClippedToWindow() {
288            return true;
289        }
290    
291        /**
292         * Draws the background to the specified graphics device.  If the dial
293         * frame specifies a window, the clipping region will already have been
294         * set to this window before this method is called.
295         *
296         * @param g2  the graphics device (<code>null</code> not permitted).
297         * @param plot  the plot (ignored here).
298         * @param frame  the dial frame (ignored here).
299         * @param view  the view rectangle (<code>null</code> not permitted).
300         */
301        public void draw(Graphics2D g2, DialPlot plot, Rectangle2D frame,
302                Rectangle2D view) {
303    
304            // work out the anchor point
305            Rectangle2D f = DialPlot.rectangleByRadius(frame, this.radius,
306                    this.radius);
307            Arc2D arc = new Arc2D.Double(f, this.angle, 0.0, Arc2D.OPEN);
308            Point2D pt = arc.getStartPoint();
309            g2.setPaint(this.paint);
310            g2.setFont(this.font);
311            TextUtilities.drawAlignedString(this.label, g2, (float) pt.getX(),
312                    (float) pt.getY(), this.anchor);
313    
314        }
315    
316        /**
317         * Tests this instance for equality with an arbitrary object.
318         *
319         * @param obj  the object (<code>null</code> permitted).
320         *
321         * @return A boolean.
322         */
323        public boolean equals(Object obj) {
324            if (obj == this) {
325                return true;
326            }
327            if (!(obj instanceof DialTextAnnotation)) {
328                return false;
329            }
330            DialTextAnnotation that = (DialTextAnnotation) obj;
331            if (!this.label.equals(that.label)) {
332                return false;
333            }
334            if (!this.font.equals(that.font)) {
335                return false;
336            }
337            if (!PaintUtilities.equal(this.paint, that.paint)) {
338                return false;
339            }
340            if (this.radius != that.radius) {
341                return false;
342            }
343            if (this.angle != that.angle) {
344                return false;
345            }
346            if (!this.anchor.equals(that.anchor)) {
347                return false;
348            }
349            return super.equals(obj);
350        }
351    
352        /**
353         * Returns a hash code for this instance.
354         *
355         * @return The hash code.
356         */
357        public int hashCode() {
358            int result = 193;
359            result = 37 * result + HashUtilities.hashCodeForPaint(this.paint);
360            result = 37 * result + this.font.hashCode();
361            result = 37 * result + this.label.hashCode();
362            result = 37 * result + this.anchor.hashCode();
363            long temp = Double.doubleToLongBits(this.angle);
364            result = 37 * result + (int) (temp ^ (temp >>> 32));
365            temp = Double.doubleToLongBits(this.radius);
366            result = 37 * result + (int) (temp ^ (temp >>> 32));
367            return result;
368        }
369    
370        /**
371         * Returns a clone of this instance.
372         *
373         * @return The clone.
374         *
375         * @throws CloneNotSupportedException if some attribute of this instance
376         *     cannot be cloned.
377         */
378        public Object clone() throws CloneNotSupportedException {
379            return super.clone();
380        }
381    
382        /**
383         * Provides serialization support.
384         *
385         * @param stream  the output stream.
386         *
387         * @throws IOException  if there is an I/O error.
388         */
389        private void writeObject(ObjectOutputStream stream) throws IOException {
390            stream.defaultWriteObject();
391            SerialUtilities.writePaint(this.paint, stream);
392        }
393    
394        /**
395         * Provides serialization support.
396         *
397         * @param stream  the input stream.
398         *
399         * @throws IOException  if there is an I/O error.
400         * @throws ClassNotFoundException  if there is a classpath problem.
401         */
402        private void readObject(ObjectInputStream stream)
403                throws IOException, ClassNotFoundException {
404            stream.defaultReadObject();
405            this.paint = SerialUtilities.readPaint(stream);
406        }
407    
408    }