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