/* ===========================================================
 * JFreeChart : a free chart library for the Java(tm) platform
 * ===========================================================
 *
 * (C) Copyright 2000-present, by David Gilbert and Contributors.
 *
 * Project Info:  http://www.jfree.org/jfreechart/index.html
 *
 * This library is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation; either version 2.1 of the License, or
 * (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
 * License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301,
 * USA.
 *
 * [Oracle and Java are registered trademarks of Oracle and/or its affiliates. 
 * Other names may be trademarks of their respective owners.]
 *
 * ----------------------
 * StandardDialScale.java
 * ----------------------
 * (C) Copyright 2006-present, by David Gilbert.
 *
 * Original Author:  David Gilbert;
 * Contributor(s):   -;
 *
 */

package org.jfree.chart.plot.dial;

import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Font;
import java.awt.Graphics2D;
import java.awt.Paint;
import java.awt.Stroke;
import java.awt.geom.Arc2D;
import java.awt.geom.Line2D;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.text.DecimalFormat;
import java.text.NumberFormat;
import org.jfree.chart.text.TextUtils;
import org.jfree.chart.ui.TextAnchor;
import org.jfree.chart.util.PaintUtils;
import org.jfree.chart.util.Args;
import org.jfree.chart.util.PublicCloneable;
import org.jfree.chart.util.SerialUtils;

/**
 * A scale for a {@link DialPlot}.
 */
public class StandardDialScale extends AbstractDialLayer implements DialScale,
        Cloneable, PublicCloneable, Serializable {

    /** For serialization. */
    static final long serialVersionUID = 3715644629665918516L;

    /** The minimum data value for the scale. */
    private double lowerBound;

    /** The maximum data value for the scale. */
    private double upperBound;

    /**
     * The start angle for the scale display, in degrees (using the same
     * encoding as Arc2D).
     */
    private double startAngle;

    /** The extent of the scale display. */
    private double extent;

    /**
     * The factor (in the range 0.0 to 1.0) that determines the outside limit
     * of the tick marks.
     */
    private double tickRadius;

    /**
     * The increment (in data units) between major tick marks.
     */
    private double majorTickIncrement;

    /**
     * The factor that is subtracted from the tickRadius to determine the
     * inner point of the major ticks.
     */
    private double majorTickLength;

    /**
     * The paint to use for major tick marks.  This field is transient because
     * it requires special handling for serialization.
     */
    private transient Paint majorTickPaint;

    /**
     * The stroke to use for major tick marks.  This field is transient because
     * it requires special handling for serialization.
     */
    private transient Stroke majorTickStroke;

    /**
     * The number of minor ticks between each major tick.
     */
    private int minorTickCount;

    /**
     * The factor that is subtracted from the tickRadius to determine the
     * inner point of the minor ticks.
     */
    private double minorTickLength;

    /**
     * The paint to use for minor tick marks.  This field is transient because
     * it requires special handling for serialization.
     */
    private transient Paint minorTickPaint;

    /**
     * The stroke to use for minor tick marks.  This field is transient because
     * it requires special handling for serialization.
     */
    private transient Stroke minorTickStroke;

    /**
     * The tick label offset.
     */
    private double tickLabelOffset;

    /**
     * The tick label font.
     */
    private Font tickLabelFont;

    /**
     * A flag that controls whether or not the tick labels are
     * displayed.
     */
    private boolean tickLabelsVisible;

    /**
     * The number formatter for the tick labels.
     */
    private NumberFormat tickLabelFormatter;

    /**
     * A flag that controls whether or not the first tick label is
     * displayed.
     */
    private boolean firstTickLabelVisible;

    /**
     * The tick label paint.  This field is transient because it requires
     * special handling for serialization.
     */
    private transient Paint tickLabelPaint;

    /**
     * Creates a new instance of DialScale.
     */
    public StandardDialScale() {
        this(0.0, 100.0, 175, -170, 10.0, 4);
    }

    /**
     * Creates a new instance.
     *
     * @param lowerBound  the lower bound of the scale.
     * @param upperBound  the upper bound of the scale.
     * @param startAngle  the start angle (in degrees, using the same
     *     orientation as Java's {@code Arc2D} class).
     * @param extent  the extent (in degrees, counter-clockwise).
     * @param majorTickIncrement  the interval between major tick marks (must
     *     be &gt; 0).
     * @param minorTickCount  the number of minor ticks between major tick
     *          marks.
     */
    public StandardDialScale(double lowerBound, double upperBound,
            double startAngle, double extent, double majorTickIncrement,
            int minorTickCount) {
        if (majorTickIncrement <= 0.0) {
            throw new IllegalArgumentException(
                    "Requires 'majorTickIncrement' > 0.");
        }
        this.startAngle = startAngle;
        this.extent = extent;
        this.lowerBound = lowerBound;
        this.upperBound = upperBound;
        this.tickRadius = 0.70;
        this.tickLabelsVisible = true;
        this.tickLabelFormatter = new DecimalFormat("0.0");
        this.firstTickLabelVisible = true;
        this.tickLabelFont = new Font("Dialog", Font.BOLD, 16);
        this.tickLabelPaint = Color.BLUE;
        this.tickLabelOffset = 0.10;
        this.majorTickIncrement = majorTickIncrement;
        this.majorTickLength = 0.04;
        this.majorTickPaint = Color.BLACK;
        this.majorTickStroke = new BasicStroke(3.0f);
        this.minorTickCount = minorTickCount;
        this.minorTickLength = 0.02;
        this.minorTickPaint = Color.BLACK;
        this.minorTickStroke = new BasicStroke(1.0f);
    }

    /**
     * Returns the lower bound for the scale.
     *
     * @return The lower bound for the scale.
     *
     * @see #setLowerBound(double)
     */
    public double getLowerBound() {
        return this.lowerBound;
    }

    /**
     * Sets the lower bound for the scale and sends a
     * {@link DialLayerChangeEvent} to all registered listeners.
     *
     * @param lower  the lower bound.
     *
     * @see #getLowerBound()
     */
    public void setLowerBound(double lower) {
        this.lowerBound = lower;
        notifyListeners(new DialLayerChangeEvent(this));
    }

    /**
     * Returns the upper bound for the scale.
     *
     * @return The upper bound for the scale.
     *
     * @see #setUpperBound(double)
     */
    public double getUpperBound() {
        return this.upperBound;
    }

    /**
     * Sets the upper bound for the scale and sends a
     * {@link DialLayerChangeEvent} to all registered listeners.
     *
     * @param upper  the upper bound.
     *
     * @see #getUpperBound()
     */
    public void setUpperBound(double upper) {
        this.upperBound = upper;
        notifyListeners(new DialLayerChangeEvent(this));
    }

    /**
     * Returns the start angle for the scale (in degrees using the same
     * orientation as Java's {@code Arc2D} class).
     *
     * @return The start angle.
     *
     * @see #setStartAngle(double)
     */
    public double getStartAngle() {
        return this.startAngle;
    }

    /**
     * Sets the start angle for the scale and sends a
     * {@link DialLayerChangeEvent} to all registered listeners.
     *
     * @param angle  the angle (in degrees).
     *
     * @see #getStartAngle()
     */
    public void setStartAngle(double angle) {
        this.startAngle = angle;
        notifyListeners(new DialLayerChangeEvent(this));
    }

    /**
     * Returns the extent.
     *
     * @return The extent.
     *
     * @see #setExtent(double)
     */
    public double getExtent() {
        return this.extent;
    }

    /**
     * Sets the extent and sends a {@link DialLayerChangeEvent} to all
     * registered listeners.
     *
     * @param extent  the extent.
     *
     * @see #getExtent()
     */
    public void setExtent(double extent) {
        this.extent = extent;
        notifyListeners(new DialLayerChangeEvent(this));
    }

    /**
     * Returns the radius (as a percentage of the maximum space available) of
     * the outer limit of the tick marks.
     *
     * @return The tick radius.
     *
     * @see #setTickRadius(double)
     */
    public double getTickRadius() {
        return this.tickRadius;
    }

    /**
     * Sets the tick radius and sends a {@link DialLayerChangeEvent} to all
     * registered listeners.
     *
     * @param radius  the radius.
     *
     * @see #getTickRadius()
     */
    public void setTickRadius(double radius) {
        if (radius <= 0.0) {
            throw new IllegalArgumentException(
                    "The 'radius' must be positive.");
        }
        this.tickRadius = radius;
        notifyListeners(new DialLayerChangeEvent(this));
    }

    /**
     * Returns the increment (in data units) between major tick labels.
     *
     * @return The increment between major tick labels.
     *
     * @see #setMajorTickIncrement(double)
     */
    public double getMajorTickIncrement() {
        return this.majorTickIncrement;
    }

    /**
     * Sets the increment (in data units) between major tick labels and sends a
     * {@link DialLayerChangeEvent} to all registered listeners.
     *
     * @param increment  the increment (must be &gt; 0).
     *
     * @see #getMajorTickIncrement()
     */
    public void setMajorTickIncrement(double increment) {
        if (increment <= 0.0) {
            throw new IllegalArgumentException(
                    "The 'increment' must be positive.");
        }
        this.majorTickIncrement = increment;
        notifyListeners(new DialLayerChangeEvent(this));
    }

    /**
     * Returns the length factor for the major tick marks.  The value is
     * subtracted from the tick radius to determine the inner starting point
     * for the tick marks.
     *
     * @return The length factor.
     *
     * @see #setMajorTickLength(double)
     */
    public double getMajorTickLength() {
        return this.majorTickLength;
    }

    /**
     * Sets the length factor for the major tick marks and sends a
     * {@link DialLayerChangeEvent} to all registered listeners.
     *
     * @param length  the length.
     *
     * @see #getMajorTickLength()
     */
    public void setMajorTickLength(double length) {
        if (length < 0.0) {
            throw new IllegalArgumentException("Negative 'length' argument.");
        }
        this.majorTickLength = length;
        notifyListeners(new DialLayerChangeEvent(this));
    }

    /**
     * Returns the major tick paint.
     *
     * @return The major tick paint (never {@code null}).
     *
     * @see #setMajorTickPaint(Paint)
     */
    public Paint getMajorTickPaint() {
        return this.majorTickPaint;
    }

    /**
     * Sets the major tick paint and sends a {@link DialLayerChangeEvent} to
     * all registered listeners.
     *
     * @param paint  the paint ({@code null} not permitted).
     *
     * @see #getMajorTickPaint()
     */
    public void setMajorTickPaint(Paint paint) {
        Args.nullNotPermitted(paint, "paint");
        this.majorTickPaint = paint;
        notifyListeners(new DialLayerChangeEvent(this));
    }

    /**
     * Returns the stroke used to draw the major tick marks.
     *
     * @return The stroke (never {@code null}).
     *
     * @see #setMajorTickStroke(Stroke)
     */
    public Stroke getMajorTickStroke() {
        return this.majorTickStroke;
    }

    /**
     * Sets the stroke used to draw the major tick marks and sends a
     * {@link DialLayerChangeEvent} to all registered listeners.
     *
     * @param stroke  the stroke ({@code null} not permitted).
     *
     * @see #getMajorTickStroke()
     */
    public void setMajorTickStroke(Stroke stroke) {
        Args.nullNotPermitted(stroke, "stroke");
        this.majorTickStroke = stroke;
        notifyListeners(new DialLayerChangeEvent(this));
    }

    /**
     * Returns the number of minor tick marks between major tick marks.
     *
     * @return The number of minor tick marks between major tick marks.
     *
     * @see #setMinorTickCount(int)
     */
    public int getMinorTickCount() {
        return this.minorTickCount;
    }

    /**
     * Sets the number of minor tick marks between major tick marks and sends
     * a {@link DialLayerChangeEvent} to all registered listeners.
     *
     * @param count  the count.
     *
     * @see #getMinorTickCount()
     */
    public void setMinorTickCount(int count) {
        if (count < 0) {
            throw new IllegalArgumentException(
                    "The 'count' cannot be negative.");
        }
        this.minorTickCount = count;
        notifyListeners(new DialLayerChangeEvent(this));
    }

    /**
     * Returns the length factor for the minor tick marks.  The value is
     * subtracted from the tick radius to determine the inner starting point
     * for the tick marks.
     *
     * @return The length factor.
     *
     * @see #setMinorTickLength(double)
     */
    public double getMinorTickLength() {
        return this.minorTickLength;
    }

    /**
     * Sets the length factor for the minor tick marks and sends
     * a {@link DialLayerChangeEvent} to all registered listeners.
     *
     * @param length  the length.
     *
     * @see #getMinorTickLength()
     */
    public void setMinorTickLength(double length) {
        if (length < 0.0) {
            throw new IllegalArgumentException("Negative 'length' argument.");
        }
        this.minorTickLength = length;
        notifyListeners(new DialLayerChangeEvent(this));
    }

    /**
     * Returns the paint used to draw the minor tick marks.
     *
     * @return The paint (never {@code null}).
     *
     * @see #setMinorTickPaint(Paint)
     */
    public Paint getMinorTickPaint() {
        return this.minorTickPaint;
    }

    /**
     * Sets the paint used to draw the minor tick marks and sends a
     * {@link DialLayerChangeEvent} to all registered listeners.
     *
     * @param paint  the paint ({@code null} not permitted).
     *
     * @see #getMinorTickPaint()
     */
    public void setMinorTickPaint(Paint paint) {
        Args.nullNotPermitted(paint, "paint");
        this.minorTickPaint = paint;
        notifyListeners(new DialLayerChangeEvent(this));
    }

    /**
     * Returns the stroke used to draw the minor tick marks.
     *
     * @return The paint (never {@code null}).
     *
     * @see #setMinorTickStroke(Stroke)
     */
    public Stroke getMinorTickStroke() {
        return this.minorTickStroke;
    }

    /**
     * Sets the stroke used to draw the minor tick marks and sends a
     * {@link DialLayerChangeEvent} to all registered listeners.
     *
     * @param stroke  the stroke ({@code null} not permitted).
     *
     * @see #getMinorTickStroke()
     */
    public void setMinorTickStroke(Stroke stroke) {
        Args.nullNotPermitted(stroke, "stroke");
        this.minorTickStroke = stroke;
        notifyListeners(new DialLayerChangeEvent(this));
    }

    /**
     * Returns the tick label offset.
     *
     * @return The tick label offset.
     *
     * @see #setTickLabelOffset(double)
     */
    public double getTickLabelOffset() {
        return this.tickLabelOffset;
    }

    /**
     * Sets the tick label offset and sends a {@link DialLayerChangeEvent} to
     * all registered listeners.
     *
     * @param offset  the offset.
     *
     * @see #getTickLabelOffset()
     */
    public void setTickLabelOffset(double offset) {
        this.tickLabelOffset = offset;
        notifyListeners(new DialLayerChangeEvent(this));
    }

    /**
     * Returns the font used to draw the tick labels.
     *
     * @return The font (never {@code null}).
     *
     * @see #setTickLabelFont(Font)
     */
    public Font getTickLabelFont() {
        return this.tickLabelFont;
    }

    /**
     * Sets the font used to display the tick labels and sends a
     * {@link DialLayerChangeEvent} to all registered listeners.
     *
     * @param font  the font ({@code null} not permitted).
     *
     * @see #getTickLabelFont()
     */
    public void setTickLabelFont(Font font) {
        Args.nullNotPermitted(font, "font");
        this.tickLabelFont = font;
        notifyListeners(new DialLayerChangeEvent(this));
    }

    /**
     * Returns the paint used to draw the tick labels.
     *
     * @return The paint ({@code null} not permitted).
     *
     * @see #setTickLabelPaint(Paint)
     */
    public Paint getTickLabelPaint() {
        return this.tickLabelPaint;
    }

    /**
     * Sets the paint used to draw the tick labels and sends a
     * {@link DialLayerChangeEvent} to all registered listeners.
     *
     * @param paint  the paint ({@code null} not permitted).
     */
    public void setTickLabelPaint(Paint paint) {
        Args.nullNotPermitted(paint, "paint");
        this.tickLabelPaint = paint;
        notifyListeners(new DialLayerChangeEvent(this));
    }

    /**
     * Returns {@code true} if the tick labels should be displayed,
     * and {@code false} otherwise.
     *
     * @return A boolean.
     *
     * @see #setTickLabelsVisible(boolean)
     */
    public boolean getTickLabelsVisible() {
        return this.tickLabelsVisible;
    }

    /**
     * Sets the flag that controls whether or not the tick labels are
     * displayed, and sends a {@link DialLayerChangeEvent} to all registered
     * listeners.
     *
     * @param visible  the new flag value.
     *
     * @see #getTickLabelsVisible()
     */
    public void setTickLabelsVisible(boolean visible) {
        this.tickLabelsVisible = visible;
        notifyListeners(new DialLayerChangeEvent(this));
    }

    /**
     * Returns the number formatter used to convert the tick label values to
     * strings.
     *
     * @return The formatter (never {@code null}).
     *
     * @see #setTickLabelFormatter(NumberFormat)
     */
    public NumberFormat getTickLabelFormatter() {
        return this.tickLabelFormatter;
    }

    /**
     * Sets the number formatter used to convert the tick label values to
     * strings, and sends a {@link DialLayerChangeEvent} to all registered
     * listeners.
     *
     * @param formatter  the formatter ({@code null} not permitted).
     *
     * @see #getTickLabelFormatter()
     */
    public void setTickLabelFormatter(NumberFormat formatter) {
        Args.nullNotPermitted(formatter, "formatter");
        this.tickLabelFormatter = formatter;
        notifyListeners(new DialLayerChangeEvent(this));
    }

    /**
     * Returns a flag that controls whether or not the first tick label is
     * visible.
     *
     * @return A boolean.
     *
     * @see #setFirstTickLabelVisible(boolean)
     */
    public boolean getFirstTickLabelVisible() {
        return this.firstTickLabelVisible;
    }

    /**
     * Sets a flag that controls whether or not the first tick label is
     * visible, and sends a {@link DialLayerChangeEvent} to all registered
     * listeners.
     *
     * @param visible  the new flag value.
     *
     * @see #getFirstTickLabelVisible()
     */
    public void setFirstTickLabelVisible(boolean visible) {
        this.firstTickLabelVisible = visible;
        notifyListeners(new DialLayerChangeEvent(this));
    }

    /**
     * Returns {@code true} to indicate that this layer should be
     * clipped within the dial window.
     *
     * @return {@code true}.
     */
    @Override
    public boolean isClippedToWindow() {
        return true;
    }

    /**
     * Draws the scale on the dial plot.
     *
     * @param g2  the graphics target ({@code null} not permitted).
     * @param plot  the dial plot ({@code null} not permitted).
     * @param frame  the reference frame that is used to construct the
     *     geometry of the plot ({@code null} not permitted).
     * @param view  the visible part of the plot ({@code null} not
     *     permitted).
     */
    @Override
    public void draw(Graphics2D g2, DialPlot plot, Rectangle2D frame,
            Rectangle2D view) {

        Rectangle2D arcRect = DialPlot.rectangleByRadius(frame,
                this.tickRadius, this.tickRadius);
        Rectangle2D arcRectMajor = DialPlot.rectangleByRadius(frame,
                this.tickRadius - this.majorTickLength,
                this.tickRadius - this.majorTickLength);
        Rectangle2D arcRectMinor = arcRect;
        if (this.minorTickCount > 0 && this.minorTickLength > 0.0) {
            arcRectMinor = DialPlot.rectangleByRadius(frame,
                    this.tickRadius - this.minorTickLength,
                    this.tickRadius - this.minorTickLength);
        }
        Rectangle2D arcRectForLabels = DialPlot.rectangleByRadius(frame,
                this.tickRadius - this.tickLabelOffset,
                this.tickRadius - this.tickLabelOffset);

        boolean firstLabel = true;

        Arc2D arc = new Arc2D.Double();
        Line2D workingLine = new Line2D.Double();
        for (double v = this.lowerBound; v <= this.upperBound;
                v += this.majorTickIncrement) {
            arc.setArc(arcRect, this.startAngle, valueToAngle(v)
                    - this.startAngle, Arc2D.OPEN);
            Point2D pt0 = arc.getEndPoint();
            arc.setArc(arcRectMajor, this.startAngle, valueToAngle(v)
                    - this.startAngle, Arc2D.OPEN);
            Point2D pt1 = arc.getEndPoint();
            g2.setPaint(this.majorTickPaint);
            g2.setStroke(this.majorTickStroke);
            workingLine.setLine(pt0, pt1);
            g2.draw(workingLine);
            arc.setArc(arcRectForLabels, this.startAngle, valueToAngle(v)
                    - this.startAngle, Arc2D.OPEN);
            Point2D pt2 = arc.getEndPoint();

            if (this.tickLabelsVisible) {
                if (!firstLabel || this.firstTickLabelVisible) {
                    g2.setFont(this.tickLabelFont);
                    g2.setPaint(this.tickLabelPaint);
                    TextUtils.drawAlignedString(
                            this.tickLabelFormatter.format(v), g2,
                            (float) pt2.getX(), (float) pt2.getY(),
                            TextAnchor.CENTER);
                }
            }
            firstLabel = false;

            // now do the minor tick marks
            if (this.minorTickCount > 0 && this.minorTickLength > 0.0) {
                double minorTickIncrement = this.majorTickIncrement
                        / (this.minorTickCount + 1);
                for (int i = 0; i < this.minorTickCount; i++) {
                    double vv = v + ((i + 1) * minorTickIncrement);
                    if (vv >= this.upperBound) {
                        break;
                    }
                    double angle = valueToAngle(vv);

                    arc.setArc(arcRect, this.startAngle, angle
                            - this.startAngle, Arc2D.OPEN);
                    pt0 = arc.getEndPoint();
                    arc.setArc(arcRectMinor, this.startAngle, angle
                            - this.startAngle, Arc2D.OPEN);
                    Point2D pt3 = arc.getEndPoint();
                    g2.setStroke(this.minorTickStroke);
                    g2.setPaint(this.minorTickPaint);
                    workingLine.setLine(pt0, pt3);
                    g2.draw(workingLine);
                }
            }

        }
    }

    /**
     * Converts a data value to an angle against this scale.
     *
     * @param value  the data value.
     *
     * @return The angle (in degrees, using the same specification as Java's
     *     Arc2D class).
     *
     * @see #angleToValue(double)
     */
    @Override
    public double valueToAngle(double value) {
        double range = this.upperBound - this.lowerBound;
        double unit = this.extent / range;
        return this.startAngle + unit * (value - this.lowerBound);
    }

    /**
     * Converts the given angle to a data value, based on this scale.
     *
     * @param angle  the angle (in degrees).
     *
     * @return The data value.
     *
     * @see #valueToAngle(double)
     */
    @Override
    public double angleToValue(double angle) {
        double range = this.upperBound - this.lowerBound;
        double unit = range / this.extent;
        return (angle - this.startAngle) * unit;
    }

    /**
     * Tests this {@code StandardDialScale} for equality with an arbitrary
     * object.
     *
     * @param obj  the object ({@code null} permitted).
     *
     * @return A boolean.
     */
    @Override
    public boolean equals(Object obj) {
        if (obj == this) {
            return true;
        }
        if (!(obj instanceof StandardDialScale)) {
            return false;
        }
        StandardDialScale that = (StandardDialScale) obj;
        if (this.lowerBound != that.lowerBound) {
            return false;
        }
        if (this.upperBound != that.upperBound) {
            return false;
        }
        if (this.startAngle != that.startAngle) {
            return false;
        }
        if (this.extent != that.extent) {
            return false;
        }
        if (this.tickRadius != that.tickRadius) {
            return false;
        }
        if (this.majorTickIncrement != that.majorTickIncrement) {
            return false;
        }
        if (this.majorTickLength != that.majorTickLength) {
            return false;
        }
        if (!PaintUtils.equal(this.majorTickPaint, that.majorTickPaint)) {
            return false;
        }
        if (!this.majorTickStroke.equals(that.majorTickStroke)) {
            return false;
        }
        if (this.minorTickCount != that.minorTickCount) {
            return false;
        }
        if (this.minorTickLength != that.minorTickLength) {
            return false;
        }
        if (!PaintUtils.equal(this.minorTickPaint, that.minorTickPaint)) {
            return false;
        }
        if (!this.minorTickStroke.equals(that.minorTickStroke)) {
            return false;
        }
        if (this.tickLabelsVisible != that.tickLabelsVisible) {
            return false;
        }
        if (this.tickLabelOffset != that.tickLabelOffset) {
            return false;
        }
        if (!this.tickLabelFont.equals(that.tickLabelFont)) {
            return false;
        }
        if (!PaintUtils.equal(this.tickLabelPaint, that.tickLabelPaint)) {
            return false;
        }
        return super.equals(obj);
    }

    /**
     * Returns a hash code for this instance.
     *
     * @return A hash code.
     */
    @Override
    public int hashCode() {
        int result = 193;
        // lowerBound
        long temp = Double.doubleToLongBits(this.lowerBound);
        result = 37 * result + (int) (temp ^ (temp >>> 32));
        // upperBound
        temp = Double.doubleToLongBits(this.upperBound);
        result = 37 * result + (int) (temp ^ (temp >>> 32));
        // startAngle
        temp = Double.doubleToLongBits(this.startAngle);
        result = 37 * result + (int) (temp ^ (temp >>> 32));
        // extent
        temp = Double.doubleToLongBits(this.extent);
        result = 37 * result + (int) (temp ^ (temp >>> 32));
        // tickRadius
        temp = Double.doubleToLongBits(this.tickRadius);
        result = 37 * result + (int) (temp ^ (temp >>> 32));
        // majorTickIncrement
        // majorTickLength
        // majorTickPaint
        // majorTickStroke
        // minorTickCount
        // minorTickLength
        // minorTickPaint
        // minorTickStroke
        // tickLabelOffset
        // tickLabelFont
        // tickLabelsVisible
        // tickLabelFormatter
        // firstTickLabelsVisible
        return result;
    }

    /**
     * Returns a clone of this instance.
     *
     * @return A clone.
     *
     * @throws CloneNotSupportedException if this instance is not cloneable.
     */
    @Override
    public Object clone() throws CloneNotSupportedException {
        return super.clone();
    }

    /**
     * Provides serialization support.
     *
     * @param stream  the output stream.
     *
     * @throws IOException  if there is an I/O error.
     */
    private void writeObject(ObjectOutputStream stream) throws IOException {
        stream.defaultWriteObject();
        SerialUtils.writePaint(this.majorTickPaint, stream);
        SerialUtils.writeStroke(this.majorTickStroke, stream);
        SerialUtils.writePaint(this.minorTickPaint, stream);
        SerialUtils.writeStroke(this.minorTickStroke, stream);
        SerialUtils.writePaint(this.tickLabelPaint, stream);
    }

    /**
     * Provides serialization support.
     *
     * @param stream  the input stream.
     *
     * @throws IOException  if there is an I/O error.
     * @throws ClassNotFoundException  if there is a classpath problem.
     */
    private void readObject(ObjectInputStream stream)
            throws IOException, ClassNotFoundException {
        stream.defaultReadObject();
        this.majorTickPaint = SerialUtils.readPaint(stream);
        this.majorTickStroke = SerialUtils.readStroke(stream);
        this.minorTickPaint = SerialUtils.readPaint(stream);
        this.minorTickStroke = SerialUtils.readStroke(stream);
        this.tickLabelPaint = SerialUtils.readPaint(stream);
    }

}
