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