001    /* ===========================================================
002     * JFreeChart : a free chart library for the Java(tm) platform
003     * ===========================================================
004     *
005     * (C) Copyright 2000-2009, 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     * StandardDialScale.java
029     * ----------------------
030     * (C) Copyright 2006-2009, 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-Nov-2006 : Added flags for tick label visibility (DG);
039     * 24-Oct-2007 : Added tick label formatter (DG);
040     * 19-Nov-2007 : Added some missing accessor methods (DG);
041     * 27-Feb-2009 : Fixed bug 2617557: tickLabelPaint ignored (DG);
042     *
043     */
044    
045    package org.jfree.chart.plot.dial;
046    
047    import java.awt.BasicStroke;
048    import java.awt.Color;
049    import java.awt.Font;
050    import java.awt.Graphics2D;
051    import java.awt.Paint;
052    import java.awt.Stroke;
053    import java.awt.geom.Arc2D;
054    import java.awt.geom.Line2D;
055    import java.awt.geom.Point2D;
056    import java.awt.geom.Rectangle2D;
057    import java.io.IOException;
058    import java.io.ObjectInputStream;
059    import java.io.ObjectOutputStream;
060    import java.io.Serializable;
061    import java.text.DecimalFormat;
062    import java.text.NumberFormat;
063    
064    import org.jfree.io.SerialUtilities;
065    import org.jfree.text.TextUtilities;
066    import org.jfree.ui.TextAnchor;
067    import org.jfree.util.PaintUtilities;
068    import org.jfree.util.PublicCloneable;
069    
070    /**
071     * A scale for a {@link DialPlot}.
072     *
073     * @since 1.0.7
074     */
075    public class StandardDialScale extends AbstractDialLayer implements DialScale,
076            Cloneable, PublicCloneable, Serializable {
077    
078        /** For serialization. */
079        static final long serialVersionUID = 3715644629665918516L;
080    
081        /** The minimum data value for the scale. */
082        private double lowerBound;
083    
084        /** The maximum data value for the scale. */
085        private double upperBound;
086    
087        /**
088         * The start angle for the scale display, in degrees (using the same
089         * encoding as Arc2D).
090         */
091        private double startAngle;
092    
093        /** The extent of the scale display. */
094        private double extent;
095    
096        /**
097         * The factor (in the range 0.0 to 1.0) that determines the outside limit
098         * of the tick marks.
099         */
100        private double tickRadius;
101    
102        /**
103         * The increment (in data units) between major tick marks.
104         */
105        private double majorTickIncrement;
106    
107        /**
108         * The factor that is subtracted from the tickRadius to determine the
109         * inner point of the major ticks.
110         */
111        private double majorTickLength;
112    
113        /**
114         * The paint to use for major tick marks.  This field is transient because
115         * it requires special handling for serialization.
116         */
117        private transient Paint majorTickPaint;
118    
119        /**
120         * The stroke to use for major tick marks.  This field is transient because
121         * it requires special handling for serialization.
122         */
123        private transient Stroke majorTickStroke;
124    
125        /**
126         * The number of minor ticks between each major tick.
127         */
128        private int minorTickCount;
129    
130        /**
131         * The factor that is subtracted from the tickRadius to determine the
132         * inner point of the minor ticks.
133         */
134        private double minorTickLength;
135    
136        /**
137         * The paint to use for minor tick marks.  This field is transient because
138         * it requires special handling for serialization.
139         */
140        private transient Paint minorTickPaint;
141    
142        /**
143         * The stroke to use for minor tick marks.  This field is transient because
144         * it requires special handling for serialization.
145         */
146        private transient Stroke minorTickStroke;
147    
148        /**
149         * The tick label offset.
150         */
151        private double tickLabelOffset;
152    
153        /**
154         * The tick label font.
155         */
156        private Font tickLabelFont;
157    
158        /**
159         * A flag that controls whether or not the tick labels are
160         * displayed.
161         */
162        private boolean tickLabelsVisible;
163    
164        /**
165         * The number formatter for the tick labels.
166         */
167        private NumberFormat tickLabelFormatter;
168    
169        /**
170         * A flag that controls whether or not the first tick label is
171         * displayed.
172         */
173        private boolean firstTickLabelVisible;
174    
175        /**
176         * The tick label paint.  This field is transient because it requires
177         * special handling for serialization.
178         */
179        private transient Paint tickLabelPaint;
180    
181        /**
182         * Creates a new instance of DialScale.
183         */
184        public StandardDialScale() {
185            this(0.0, 100.0, 175, -170, 10.0, 4);
186        }
187    
188        /**
189         * Creates a new instance.
190         *
191         * @param lowerBound  the lower bound of the scale.
192         * @param upperBound  the upper bound of the scale.
193         * @param startAngle  the start angle (in degrees, using the same
194         *     orientation as Java's <code>Arc2D</code> class).
195         * @param extent  the extent (in degrees, counter-clockwise).
196         * @param majorTickIncrement  the interval between major tick marks
197         * @param minorTickCount  the number of minor ticks between major tick
198         *          marks.
199         */
200        public StandardDialScale(double lowerBound, double upperBound,
201                double startAngle, double extent, double majorTickIncrement,
202                int minorTickCount) {
203            this.startAngle = startAngle;
204            this.extent = extent;
205            this.lowerBound = lowerBound;
206            this.upperBound = upperBound;
207            this.tickRadius = 0.70;
208            this.tickLabelsVisible = true;
209            this.tickLabelFormatter = new DecimalFormat("0.0");
210            this.firstTickLabelVisible = true;
211            this.tickLabelFont = new Font("Dialog", Font.BOLD, 16);
212            this.tickLabelPaint = Color.blue;
213            this.tickLabelOffset = 0.10;
214            this.majorTickIncrement = majorTickIncrement;
215            this.majorTickLength = 0.04;
216            this.majorTickPaint = Color.black;
217            this.majorTickStroke = new BasicStroke(3.0f);
218            this.minorTickCount = minorTickCount;
219            this.minorTickLength = 0.02;
220            this.minorTickPaint = Color.black;
221            this.minorTickStroke = new BasicStroke(1.0f);
222        }
223    
224        /**
225         * Returns the lower bound for the scale.
226         *
227         * @return The lower bound for the scale.
228         *
229         * @see #setLowerBound(double)
230         *
231         * @since 1.0.8
232         */
233        public double getLowerBound() {
234            return this.lowerBound;
235        }
236    
237        /**
238         * Sets the lower bound for the scale and sends a
239         * {@link DialLayerChangeEvent} to all registered listeners.
240         *
241         * @param lower  the lower bound.
242         *
243         * @see #getLowerBound()
244         *
245         * @since 1.0.8
246         */
247        public void setLowerBound(double lower) {
248            this.lowerBound = lower;
249            notifyListeners(new DialLayerChangeEvent(this));
250        }
251    
252        /**
253         * Returns the upper bound for the scale.
254         *
255         * @return The upper bound for the scale.
256         *
257         * @see #setUpperBound(double)
258         *
259         * @since 1.0.8
260         */
261        public double getUpperBound() {
262            return this.upperBound;
263        }
264    
265        /**
266         * Sets the upper bound for the scale and sends a
267         * {@link DialLayerChangeEvent} to all registered listeners.
268         *
269         * @param upper  the upper bound.
270         *
271         * @see #getUpperBound()
272         *
273         * @since 1.0.8
274         */
275        public void setUpperBound(double upper) {
276            this.upperBound = upper;
277            notifyListeners(new DialLayerChangeEvent(this));
278        }
279    
280        /**
281         * Returns the start angle for the scale (in degrees using the same
282         * orientation as Java's <code>Arc2D</code> class).
283         *
284         * @return The start angle.
285         *
286         * @see #setStartAngle(double)
287         */
288        public double getStartAngle() {
289            return this.startAngle;
290        }
291    
292        /**
293         * Sets the start angle for the scale and sends a
294         * {@link DialLayerChangeEvent} to all registered listeners.
295         *
296         * @param angle  the angle (in degrees).
297         *
298         * @see #getStartAngle()
299         */
300        public void setStartAngle(double angle) {
301            this.startAngle = angle;
302            notifyListeners(new DialLayerChangeEvent(this));
303        }
304    
305        /**
306         * Returns the extent.
307         *
308         * @return The extent.
309         *
310         * @see #setExtent(double)
311         */
312        public double getExtent() {
313            return this.extent;
314        }
315    
316        /**
317         * Sets the extent and sends a {@link DialLayerChangeEvent} to all
318         * registered listeners.
319         *
320         * @param extent  the extent.
321         *
322         * @see #getExtent()
323         */
324        public void setExtent(double extent) {
325            this.extent = extent;
326            notifyListeners(new DialLayerChangeEvent(this));
327        }
328    
329        /**
330         * Returns the radius (as a percentage of the maximum space available) of
331         * the outer limit of the tick marks.
332         *
333         * @return The tick radius.
334         *
335         * @see #setTickRadius(double)
336         */
337        public double getTickRadius() {
338            return this.tickRadius;
339        }
340    
341        /**
342         * Sets the tick radius and sends a {@link DialLayerChangeEvent} to all
343         * registered listeners.
344         *
345         * @param radius  the radius.
346         *
347         * @see #getTickRadius()
348         */
349        public void setTickRadius(double radius) {
350            if (radius <= 0.0) {
351                throw new IllegalArgumentException(
352                        "The 'radius' must be positive.");
353            }
354            this.tickRadius = radius;
355            notifyListeners(new DialLayerChangeEvent(this));
356        }
357    
358        /**
359         * Returns the increment (in data units) between major tick labels.
360         *
361         * @return The increment between major tick labels.
362         *
363         * @see #setMajorTickIncrement(double)
364         */
365        public double getMajorTickIncrement() {
366            return this.majorTickIncrement;
367        }
368    
369        /**
370         * Sets the increment (in data units) between major tick labels and sends a
371         * {@link DialLayerChangeEvent} to all registered listeners.
372         *
373         * @param increment  the increment.
374         *
375         * @see #getMajorTickIncrement()
376         */
377        public void setMajorTickIncrement(double increment) {
378            if (increment <= 0.0) {
379                throw new IllegalArgumentException(
380                        "The 'increment' must be positive.");
381            }
382            this.majorTickIncrement = increment;
383            notifyListeners(new DialLayerChangeEvent(this));
384        }
385    
386        /**
387         * Returns the length factor for the major tick marks.  The value is
388         * subtracted from the tick radius to determine the inner starting point
389         * for the tick marks.
390         *
391         * @return The length factor.
392         *
393         * @see #setMajorTickLength(double)
394         */
395        public double getMajorTickLength() {
396            return this.majorTickLength;
397        }
398    
399        /**
400         * Sets the length factor for the major tick marks and sends a
401         * {@link DialLayerChangeEvent} to all registered listeners.
402         *
403         * @param length  the length.
404         *
405         * @see #getMajorTickLength()
406         */
407        public void setMajorTickLength(double length) {
408            if (length < 0.0) {
409                throw new IllegalArgumentException("Negative 'length' argument.");
410            }
411            this.majorTickLength = length;
412            notifyListeners(new DialLayerChangeEvent(this));
413        }
414    
415        /**
416         * Returns the major tick paint.
417         *
418         * @return The major tick paint (never <code>null</code>).
419         *
420         * @see #setMajorTickPaint(Paint)
421         */
422        public Paint getMajorTickPaint() {
423            return this.majorTickPaint;
424        }
425    
426        /**
427         * Sets the major tick paint and sends a {@link DialLayerChangeEvent} to
428         * all registered listeners.
429         *
430         * @param paint  the paint (<code>null</code> not permitted).
431         *
432         * @see #getMajorTickPaint()
433         */
434        public void setMajorTickPaint(Paint paint) {
435            if (paint == null) {
436                throw new IllegalArgumentException("Null 'paint' argument.");
437            }
438            this.majorTickPaint = paint;
439            notifyListeners(new DialLayerChangeEvent(this));
440        }
441    
442        /**
443         * Returns the stroke used to draw the major tick marks.
444         *
445         * @return The stroke (never <code>null</code>).
446         *
447         * @see #setMajorTickStroke(Stroke)
448         */
449        public Stroke getMajorTickStroke() {
450            return this.majorTickStroke;
451        }
452    
453        /**
454         * Sets the stroke used to draw the major tick marks and sends a
455         * {@link DialLayerChangeEvent} to all registered listeners.
456         *
457         * @param stroke  the stroke (<code>null</code> not permitted).
458         *
459         * @see #getMajorTickStroke()
460         */
461        public void setMajorTickStroke(Stroke stroke) {
462            if (stroke == null) {
463                throw new IllegalArgumentException("Null 'stroke' argument.");
464            }
465            this.majorTickStroke = stroke;
466            notifyListeners(new DialLayerChangeEvent(this));
467        }
468    
469        /**
470         * Returns the number of minor tick marks between major tick marks.
471         *
472         * @return The number of minor tick marks between major tick marks.
473         *
474         * @see #setMinorTickCount(int)
475         */
476        public int getMinorTickCount() {
477            return this.minorTickCount;
478        }
479    
480        /**
481         * Sets the number of minor tick marks between major tick marks and sends
482         * a {@link DialLayerChangeEvent} to all registered listeners.
483         *
484         * @param count  the count.
485         *
486         * @see #getMinorTickCount()
487         */
488        public void setMinorTickCount(int count) {
489            if (count < 0) {
490                throw new IllegalArgumentException(
491                        "The 'count' cannot be negative.");
492            }
493            this.minorTickCount = count;
494            notifyListeners(new DialLayerChangeEvent(this));
495        }
496    
497        /**
498         * Returns the length factor for the minor tick marks.  The value is
499         * subtracted from the tick radius to determine the inner starting point
500         * for the tick marks.
501         *
502         * @return The length factor.
503         *
504         * @see #setMinorTickLength(double)
505         */
506        public double getMinorTickLength() {
507            return this.minorTickLength;
508        }
509    
510        /**
511         * Sets the length factor for the minor tick marks and sends
512         * a {@link DialLayerChangeEvent} to all registered listeners.
513         *
514         * @param length  the length.
515         *
516         * @see #getMinorTickLength()
517         */
518        public void setMinorTickLength(double length) {
519            if (length < 0.0) {
520                throw new IllegalArgumentException("Negative 'length' argument.");
521            }
522            this.minorTickLength = length;
523            notifyListeners(new DialLayerChangeEvent(this));
524        }
525    
526        /**
527         * Returns the paint used to draw the minor tick marks.
528         *
529         * @return The paint (never <code>null</code>).
530         *
531         * @see #setMinorTickPaint(Paint)
532         */
533        public Paint getMinorTickPaint() {
534            return this.minorTickPaint;
535        }
536    
537        /**
538         * Sets the paint used to draw the minor tick marks and sends a
539         * {@link DialLayerChangeEvent} to all registered listeners.
540         *
541         * @param paint  the paint (<code>null</code> not permitted).
542         *
543         * @see #getMinorTickPaint()
544         */
545        public void setMinorTickPaint(Paint paint) {
546            if (paint == null) {
547                throw new IllegalArgumentException("Null 'paint' argument.");
548            }
549            this.minorTickPaint = paint;
550            notifyListeners(new DialLayerChangeEvent(this));
551        }
552    
553        /**
554         * Returns the stroke used to draw the minor tick marks.
555         *
556         * @return The paint (never <code>null</code>).
557         *
558         * @see #setMinorTickStroke(Stroke)
559         *
560         * @since 1.0.8
561         */
562        public Stroke getMinorTickStroke() {
563            return this.minorTickStroke;
564        }
565    
566        /**
567         * Sets the stroke used to draw the minor tick marks and sends a
568         * {@link DialLayerChangeEvent} to all registered listeners.
569         *
570         * @param stroke  the stroke (<code>null</code> not permitted).
571         *
572         * @see #getMinorTickStroke()
573         *
574         * @since 1.0.8
575         */
576        public void setMinorTickStroke(Stroke stroke) {
577            if (stroke == null) {
578                throw new IllegalArgumentException("Null 'stroke' argument.");
579            }
580            this.minorTickStroke = stroke;
581            notifyListeners(new DialLayerChangeEvent(this));
582        }
583    
584        /**
585         * Returns the tick label offset.
586         *
587         * @return The tick label offset.
588         *
589         * @see #setTickLabelOffset(double)
590         */
591        public double getTickLabelOffset() {
592            return this.tickLabelOffset;
593        }
594    
595        /**
596         * Sets the tick label offset and sends a {@link DialLayerChangeEvent} to
597         * all registered listeners.
598         *
599         * @param offset  the offset.
600         *
601         * @see #getTickLabelOffset()
602         */
603        public void setTickLabelOffset(double offset) {
604            this.tickLabelOffset = offset;
605            notifyListeners(new DialLayerChangeEvent(this));
606        }
607    
608        /**
609         * Returns the font used to draw the tick labels.
610         *
611         * @return The font (never <code>null</code>).
612         *
613         * @see #setTickLabelFont(Font)
614         */
615        public Font getTickLabelFont() {
616            return this.tickLabelFont;
617        }
618    
619        /**
620         * Sets the font used to display the tick labels and sends a
621         * {@link DialLayerChangeEvent} to all registered listeners.
622         *
623         * @param font  the font (<code>null</code> not permitted).
624         *
625         * @see #getTickLabelFont()
626         */
627        public void setTickLabelFont(Font font) {
628            if (font == null) {
629                throw new IllegalArgumentException("Null 'font' argument.");
630            }
631            this.tickLabelFont = font;
632            notifyListeners(new DialLayerChangeEvent(this));
633        }
634    
635        /**
636         * Returns the paint used to draw the tick labels.
637         *
638         * @return The paint (<code>null</code> not permitted).
639         *
640         * @see #setTickLabelPaint(Paint)
641         */
642        public Paint getTickLabelPaint() {
643            return this.tickLabelPaint;
644        }
645    
646        /**
647         * Sets the paint used to draw the tick labels and sends a
648         * {@link DialLayerChangeEvent} to all registered listeners.
649         *
650         * @param paint  the paint (<code>null</code> not permitted).
651         */
652        public void setTickLabelPaint(Paint paint) {
653            if (paint == null) {
654                throw new IllegalArgumentException("Null 'paint' argument.");
655            }
656            this.tickLabelPaint = paint;
657            notifyListeners(new DialLayerChangeEvent(this));
658        }
659    
660        /**
661         * Returns <code>true</code> if the tick labels should be displayed,
662         * and <code>false</code> otherwise.
663         *
664         * @return A boolean.
665         *
666         * @see #setTickLabelsVisible(boolean)
667         */
668        public boolean getTickLabelsVisible() {
669            return this.tickLabelsVisible;
670        }
671    
672        /**
673         * Sets the flag that controls whether or not the tick labels are
674         * displayed, and sends a {@link DialLayerChangeEvent} to all registered
675         * listeners.
676         *
677         * @param visible  the new flag value.
678         *
679         * @see #getTickLabelsVisible()
680         */
681        public void setTickLabelsVisible(boolean visible) {
682            this.tickLabelsVisible = visible;
683            notifyListeners(new DialLayerChangeEvent(this));
684        }
685    
686        /**
687         * Returns the number formatter used to convert the tick label values to
688         * strings.
689         *
690         * @return The formatter (never <code>null</code>).
691         *
692         * @see #setTickLabelFormatter(NumberFormat)
693         */
694        public NumberFormat getTickLabelFormatter() {
695            return this.tickLabelFormatter;
696        }
697    
698        /**
699         * Sets the number formatter used to convert the tick label values to
700         * strings, and sends a {@link DialLayerChangeEvent} to all registered
701         * listeners.
702         *
703         * @param formatter  the formatter (<code>null</code> not permitted).
704         *
705         * @see #getTickLabelFormatter()
706         */
707        public void setTickLabelFormatter(NumberFormat formatter) {
708            if (formatter == null) {
709                throw new IllegalArgumentException("Null 'formatter' argument.");
710            }
711            this.tickLabelFormatter = formatter;
712            notifyListeners(new DialLayerChangeEvent(this));
713        }
714    
715        /**
716         * Returns a flag that controls whether or not the first tick label is
717         * visible.
718         *
719         * @return A boolean.
720         *
721         * @see #setFirstTickLabelVisible(boolean)
722         */
723        public boolean getFirstTickLabelVisible() {
724            return this.firstTickLabelVisible;
725        }
726    
727        /**
728         * Sets a flag that controls whether or not the first tick label is
729         * visible, and sends a {@link DialLayerChangeEvent} to all registered
730         * listeners.
731         *
732         * @param visible  the new flag value.
733         *
734         * @see #getFirstTickLabelVisible()
735         */
736        public void setFirstTickLabelVisible(boolean visible) {
737            this.firstTickLabelVisible = visible;
738            notifyListeners(new DialLayerChangeEvent(this));
739        }
740    
741        /**
742         * Returns <code>true</code> to indicate that this layer should be
743         * clipped within the dial window.
744         *
745         * @return <code>true</code>.
746         */
747        public boolean isClippedToWindow() {
748            return true;
749        }
750    
751        /**
752         * Draws the scale on the dial plot.
753         *
754         * @param g2  the graphics target (<code>null</code> not permitted).
755         * @param plot  the dial plot (<code>null</code> not permitted).
756         * @param frame  the reference frame that is used to construct the
757         *     geometry of the plot (<code>null</code> not permitted).
758         * @param view  the visible part of the plot (<code>null</code> not
759         *     permitted).
760         */
761        public void draw(Graphics2D g2, DialPlot plot, Rectangle2D frame,
762                Rectangle2D view) {
763    
764            Rectangle2D arcRect = DialPlot.rectangleByRadius(frame,
765                    this.tickRadius, this.tickRadius);
766            Rectangle2D arcRectMajor = DialPlot.rectangleByRadius(frame,
767                    this.tickRadius - this.majorTickLength,
768                    this.tickRadius - this.majorTickLength);
769            Rectangle2D arcRectMinor = arcRect;
770            if (this.minorTickCount > 0 && this.minorTickLength > 0.0) {
771                arcRectMinor = DialPlot.rectangleByRadius(frame,
772                        this.tickRadius - this.minorTickLength,
773                        this.tickRadius - this.minorTickLength);
774            }
775            Rectangle2D arcRectForLabels = DialPlot.rectangleByRadius(frame,
776                    this.tickRadius - this.tickLabelOffset,
777                    this.tickRadius - this.tickLabelOffset);
778    
779            boolean firstLabel = true;
780    
781            Arc2D arc = new Arc2D.Double();
782            Line2D workingLine = new Line2D.Double();
783            for (double v = this.lowerBound; v <= this.upperBound;
784                    v += this.majorTickIncrement) {
785                arc.setArc(arcRect, this.startAngle, valueToAngle(v)
786                        - this.startAngle, Arc2D.OPEN);
787                Point2D pt0 = arc.getEndPoint();
788                arc.setArc(arcRectMajor, this.startAngle, valueToAngle(v)
789                        - this.startAngle, Arc2D.OPEN);
790                Point2D pt1 = arc.getEndPoint();
791                g2.setPaint(this.majorTickPaint);
792                g2.setStroke(this.majorTickStroke);
793                workingLine.setLine(pt0, pt1);
794                g2.draw(workingLine);
795                arc.setArc(arcRectForLabels, this.startAngle, valueToAngle(v)
796                        - this.startAngle, Arc2D.OPEN);
797                Point2D pt2 = arc.getEndPoint();
798    
799                if (this.tickLabelsVisible) {
800                    if (!firstLabel || this.firstTickLabelVisible) {
801                        g2.setFont(this.tickLabelFont);
802                        g2.setPaint(this.tickLabelPaint);
803                        TextUtilities.drawAlignedString(
804                                this.tickLabelFormatter.format(v), g2,
805                                (float) pt2.getX(), (float) pt2.getY(),
806                                TextAnchor.CENTER);
807                    }
808                }
809                firstLabel = false;
810    
811                // now do the minor tick marks
812                if (this.minorTickCount > 0 && this.minorTickLength > 0.0) {
813                    double minorTickIncrement = this.majorTickIncrement
814                            / (this.minorTickCount + 1);
815                    for (int i = 0; i < this.minorTickCount; i++) {
816                        double vv = v + ((i + 1) * minorTickIncrement);
817                        if (vv >= this.upperBound) {
818                            break;
819                        }
820                        double angle = valueToAngle(vv);
821    
822                        arc.setArc(arcRect, this.startAngle, angle
823                                - this.startAngle, Arc2D.OPEN);
824                        pt0 = arc.getEndPoint();
825                        arc.setArc(arcRectMinor, this.startAngle, angle
826                                - this.startAngle, Arc2D.OPEN);
827                        Point2D pt3 = arc.getEndPoint();
828                        g2.setStroke(this.minorTickStroke);
829                        g2.setPaint(this.minorTickPaint);
830                        workingLine.setLine(pt0, pt3);
831                        g2.draw(workingLine);
832                    }
833                }
834    
835            }
836        }
837    
838        /**
839         * Converts a data value to an angle against this scale.
840         *
841         * @param value  the data value.
842         *
843         * @return The angle (in degrees, using the same specification as Java's
844         *     Arc2D class).
845         *
846         * @see #angleToValue(double)
847         */
848        public double valueToAngle(double value) {
849            double range = this.upperBound - this.lowerBound;
850            double unit = this.extent / range;
851            return this.startAngle + unit * (value - this.lowerBound);
852        }
853    
854        /**
855         * Converts the given angle to a data value, based on this scale.
856         *
857         * @param angle  the angle.
858         *
859         * @return The data value.
860         *
861         * @see #valueToAngle(double)
862         */
863        public double angleToValue(double angle) {
864            return Double.NaN;  // FIXME
865        }
866    
867        /**
868         * Tests this <code>StandardDialScale</code> for equality with an arbitrary
869         * object.
870         *
871         * @param obj  the object (<code>null</code> permitted).
872         *
873         * @return A boolean.
874         */
875        public boolean equals(Object obj) {
876            if (obj == this) {
877                return true;
878            }
879            if (!(obj instanceof StandardDialScale)) {
880                return false;
881            }
882            StandardDialScale that = (StandardDialScale) obj;
883            if (this.lowerBound != that.lowerBound) {
884                return false;
885            }
886            if (this.upperBound != that.upperBound) {
887                return false;
888            }
889            if (this.startAngle != that.startAngle) {
890                return false;
891            }
892            if (this.extent != that.extent) {
893                return false;
894            }
895            if (this.tickRadius != that.tickRadius) {
896                return false;
897            }
898            if (this.majorTickIncrement != that.majorTickIncrement) {
899                return false;
900            }
901            if (this.majorTickLength != that.majorTickLength) {
902                return false;
903            }
904            if (!PaintUtilities.equal(this.majorTickPaint, that.majorTickPaint)) {
905                return false;
906            }
907            if (!this.majorTickStroke.equals(that.majorTickStroke)) {
908                return false;
909            }
910            if (this.minorTickCount != that.minorTickCount) {
911                return false;
912            }
913            if (this.minorTickLength != that.minorTickLength) {
914                return false;
915            }
916            if (!PaintUtilities.equal(this.minorTickPaint, that.minorTickPaint)) {
917                return false;
918            }
919            if (!this.minorTickStroke.equals(that.minorTickStroke)) {
920                return false;
921            }
922            if (this.tickLabelsVisible != that.tickLabelsVisible) {
923                return false;
924            }
925            if (this.tickLabelOffset != that.tickLabelOffset) {
926                return false;
927            }
928            if (!this.tickLabelFont.equals(that.tickLabelFont)) {
929                return false;
930            }
931            if (!PaintUtilities.equal(this.tickLabelPaint, that.tickLabelPaint)) {
932                return false;
933            }
934            return super.equals(obj);
935        }
936    
937        /**
938         * Returns a hash code for this instance.
939         *
940         * @return A hash code.
941         */
942        public int hashCode() {
943            int result = 193;
944            // lowerBound
945            long temp = Double.doubleToLongBits(this.lowerBound);
946            result = 37 * result + (int) (temp ^ (temp >>> 32));
947            // upperBound
948            temp = Double.doubleToLongBits(this.upperBound);
949            result = 37 * result + (int) (temp ^ (temp >>> 32));
950            // startAngle
951            temp = Double.doubleToLongBits(this.startAngle);
952            result = 37 * result + (int) (temp ^ (temp >>> 32));
953            // extent
954            temp = Double.doubleToLongBits(this.extent);
955            result = 37 * result + (int) (temp ^ (temp >>> 32));
956            // tickRadius
957            temp = Double.doubleToLongBits(this.tickRadius);
958            result = 37 * result + (int) (temp ^ (temp >>> 32));
959            // majorTickIncrement
960            // majorTickLength
961            // majorTickPaint
962            // majorTickStroke
963            // minorTickCount
964            // minorTickLength
965            // minorTickPaint
966            // minorTickStroke
967            // tickLabelOffset
968            // tickLabelFont
969            // tickLabelsVisible
970            // tickLabelFormatter
971            // firstTickLabelsVisible
972            return result;
973        }
974    
975        /**
976         * Returns a clone of this instance.
977         *
978         * @return A clone.
979         *
980         * @throws CloneNotSupportedException if this instance is not cloneable.
981         */
982        public Object clone() throws CloneNotSupportedException {
983            return super.clone();
984        }
985    
986        /**
987         * Provides serialization support.
988         *
989         * @param stream  the output stream.
990         *
991         * @throws IOException  if there is an I/O error.
992         */
993        private void writeObject(ObjectOutputStream stream) throws IOException {
994            stream.defaultWriteObject();
995            SerialUtilities.writePaint(this.majorTickPaint, stream);
996            SerialUtilities.writeStroke(this.majorTickStroke, stream);
997            SerialUtilities.writePaint(this.minorTickPaint, stream);
998            SerialUtilities.writeStroke(this.minorTickStroke, stream);
999            SerialUtilities.writePaint(this.tickLabelPaint, stream);
1000        }
1001    
1002        /**
1003         * Provides serialization support.
1004         *
1005         * @param stream  the input stream.
1006         *
1007         * @throws IOException  if there is an I/O error.
1008         * @throws ClassNotFoundException  if there is a classpath problem.
1009         */
1010        private void readObject(ObjectInputStream stream)
1011                throws IOException, ClassNotFoundException {
1012            stream.defaultReadObject();
1013            this.majorTickPaint = SerialUtilities.readPaint(stream);
1014            this.majorTickStroke = SerialUtilities.readStroke(stream);
1015            this.minorTickPaint = SerialUtilities.readPaint(stream);
1016            this.minorTickStroke = SerialUtilities.readStroke(stream);
1017            this.tickLabelPaint = SerialUtilities.readPaint(stream);
1018        }
1019    
1020    }