    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    }