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