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