    001/* ===========================================================
    002 * Orson Charts : a 3D chart library for the Java(tm) platform
    003 * ===========================================================
    004 *
    005 * (C)opyright 2013-2022, by David Gilbert.  All rights reserved.
    006 *
    007 * https://github.com/jfree/orson-charts
    008 *
    009 * This program is free software: you can redistribute it and/or modify
    010 * it under the terms of the GNU General Public License as published by
    011 * the Free Software Foundation, either version 3 of the License, or
    012 * (at your option) any later version.
    013 *
    014 * This program is distributed in the hope that it will be useful,
    015 * but WITHOUT ANY WARRANTY; without even the implied warranty of
    016 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    017 * GNU General Public License for more details.
    018 *
    019 * You should have received a copy of the GNU General Public License
    020 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
    021 *
    022 * [Oracle and Java are registered trademarks of Oracle and/or its affiliates.
    023 * Other names may be trademarks of their respective owners.]
    024 *
    025 * If you do not wish to be bound by the terms of the GPL, an alternative
    026 * commercial license can be purchased.  For details, please see visit the
    027 * Orson Charts home page:
    028 *
    029 * http://www.object-refinery.com/orsoncharts/index.html
    030 *
    031 */
    032
    033package org.jfree.chart3d.axis;
    034
    035import java.awt.FontMetrics;
    036import java.awt.Graphics2D;
    037import java.awt.Shape;
    038import java.awt.geom.Line2D;
    039import java.awt.geom.Point2D;
    040import java.awt.font.LineMetrics;
    041import java.text.DecimalFormat;
    042import java.text.Format;
    043import java.io.Serializable;
    044import java.util.ArrayList;
    045import java.util.HashMap;
    046import java.util.Map;
    047import java.util.List;
    048
    049import org.jfree.chart3d.Chart3DHints;
    050import org.jfree.chart3d.data.Range;
    051import org.jfree.chart3d.graphics2d.TextAnchor;
    052import org.jfree.chart3d.graphics3d.RenderedElement;
    053import org.jfree.chart3d.graphics3d.RenderingInfo;
    054import org.jfree.chart3d.graphics3d.internal.Utils2D;
    055import org.jfree.chart3d.interaction.InteractiveElementType;
    056import org.jfree.chart3d.internal.TextUtils;
    057import org.jfree.chart3d.internal.Args;
    058import org.jfree.chart3d.internal.ObjectUtils;
    059import org.jfree.chart3d.plot.CategoryPlot3D;
    060import org.jfree.chart3d.plot.XYZPlot;
    061
    062/**
    063 * A numerical axis for use with 3D plots (implements {@link ValueAxis3D}).
    064 * In a {@link CategoryPlot3D} the value axis (the vertical one) is numerical,
    065 * and in an {@link XYZPlot} all the axes (x, y and z) are numerical - for
    066 * all these cases an instance of this class can be used.
    067 * <br><br>
    068 * NOTE: This class is serializable, but the serialization format is subject
    069 * to change in future releases and should not be relied upon for persisting
    070 * instances of this class.
    071 */
    072public class NumberAxis3D extends AbstractValueAxis3D implements ValueAxis3D,
    073        Serializable {
    074
    075    /**
    076     * Default formatter for axis number values. Can be overwritten.
    077     */
    078    private static final Format DEFAULT_TICK_LABEL_FORMATTER = new DecimalFormat("0.00");
    079
    080    /**
    081     * A flag indicating whether or not the auto-range calculation should
    082     * include zero.
    083     */
    084    private boolean autoRangeIncludesZero;
    085
    086    /**
    087     * A flag that controls how zero is handled when it falls within the
    088     * margins.  If {@code true}, the margin is truncated at zero, if
    089     * {@code false} the margin is not changed.
    090     */
    091    private boolean autoRangeStickyZero;
    092
    093    /**
    094     * The tick selector (if not {@code null}, then auto-tick selection is
    095     * used).
    096     */
    097    private TickSelector tickSelector;
    098
    099    /**
    100     * The tick size.  If the tickSelector is not {@code null} then it is
    101     * used to auto-select an appropriate tick size and format.
    102     */
    103    private double tickSize;
    104
    105    /** The tick formatter (never {@code null}). */
    106    private Format tickLabelFormatter;
    107
    108    /**
    109     * Creates a new axis with the specified label and default attributes.
    110     *
    111     * @param label  the axis label ({@code null} permitted).
    112     */
    113    public NumberAxis3D(String label) {
    114        this(label, new Range(0.0, 1.0));
    115    }
    116
    117    /**
    118     * Creates a new axis with the specified label and range.
    119     *
    120     * @param label  the axis label ({@code null} permitted).
    121     * @param range  the range ({@code null} not permitted).
    122     */
    123    public NumberAxis3D(String label, Range range) {
    124        super(label, range);
    125        this.autoRangeIncludesZero = false;
    126        this.autoRangeStickyZero = true;
    127        this.tickSelector = new NumberTickSelector();
    128        this.tickSize = range.getLength() / 10.0;
    129        this.tickLabelFormatter = DEFAULT_TICK_LABEL_FORMATTER;
    130    }
    131
    132    /**
    133     * Returns the flag that determines whether or not the auto range
    134     * mechanism should force zero to be included in the range.  The default
    135     * value is {@code false}.
    136     *
    137     * @return A boolean.
    138     */
    139    public boolean getAutoRangeIncludesZero() {
    140        return this.autoRangeIncludesZero;
    141    }
    142
    143    /**
    144     * Sets the flag that controls whether or not the auto range mechanism
    145     * should force zero to be included in the axis range, and sends an
    146     * {@link Axis3DChangeEvent} to all registered listeners.
    147     *
    148     * @param include  the new flag value.
    149     */
    150    public void setAutoRangeIncludeZero(boolean include) {
    151        this.autoRangeIncludesZero = include;
    152        fireChangeEvent(true);
    153    }
    154
    155    /**
    156     * Returns the flag that controls the behaviour of the auto range
    157     * mechanism when zero falls into the axis margins.  The default value
    158     * is {@code true}.
    159     *
    160     * @return A boolean.
    161     *
    162     * @see #setAutoRangeStickyZero(boolean)
    163     */
    164    public boolean getAutoRangeStickyZero() {
    165        return this.autoRangeStickyZero;
    166    }
    167
    168    /**
    169     * Sets the flag that controls the behaviour of the auto range mechanism
    170     * when zero falls into the axis margins.  If {@code true}, when
    171     * zero is in the axis margin the axis range is truncated at zero.  If
    172     * {@code false}, there is no special treatment.
    173     *
    174     * @param sticky  the new flag value.
    175     */
    176    public void setAutoRangeStickyZero(boolean sticky) {
    177        this.autoRangeStickyZero = sticky;
    178        fireChangeEvent(true);
    179    }
    180
    181    /**
    182     * Returns the tick selector, an object that is responsible for choosing
    183     * standard tick units for the axis.  The default value is a default
    184     * instance of {@link NumberTickSelector}.
    185     *
    186     * @return The tick selector.
    187     *
    188     * @see #setTickSelector(TickSelector)
    189     */
    190    public TickSelector getTickSelector() {
    191        return this.tickSelector;
    192    }
    193
    194    /**
    195     * Sets the tick selector and sends an {@link Axis3DChangeEvent} to all
    196     * registered listeners.
    197     *
    198     * @param selector  the selector ({@code null} permitted).
    199     *
    200     * @see #getTickSelector()
    201     */
    202    public void setTickSelector(TickSelector selector) {
    203        this.tickSelector = selector;
    204        fireChangeEvent(false);
    205    }
    206
    207    /**
    208     * Returns the tick size (to be used when the tick selector is
    209     * {@code null}).
    210     *
    211     * @return The tick size.
    212     */
    213    public double getTickSize() {
    214        return this.tickSize;
    215    }
    216
    217    /**
    218     * Sets the tick size and sends an {@link Axis3DChangeEvent} to all
    219     * registered listeners.
    220     *
    221     * @param tickSize  the new tick size.
    222     */
    223    public void setTickSize(double tickSize) {
    224        this.tickSize = tickSize;
    225        fireChangeEvent(false);
    226    }
    227
    228    /**
    229     * Returns the tick label formatter.  The default value is
    230     * {@code DecimalFormat("0.00")}.
    231     *
    232     * @return The tick label formatter (never {@code null}).
    233     */
    234    public Format getTickLabelFormatter() {
    235        return this.tickLabelFormatter;
    236    }
    237
    238    /**
    239     * Sets the formatter for the tick labels and sends an
    240     * {@link Axis3DChangeEvent} to all registered listeners.
    241     *
    242     * @param formatter  the formatter ({@code null} not permitted).
    243     */
    244    public void setTickLabelFormatter(Format formatter) {
    245        Args.nullNotPermitted(formatter, "formatter");
    246        this.tickLabelFormatter = formatter;
    247        fireChangeEvent(false);
    248    }
    249
    250    /**
    251     * Adjusts the range by adding the lower and upper margins and taking into
    252     * account also the {@code autoRangeStickyZero} flag.
    253     *
    254     * @param range  the range ({@code null} not permitted).
    255     *
    256     * @return The adjusted range.
    257     */
    258    @Override
    259    protected Range adjustedDataRange(Range range) {
    260        Args.nullNotPermitted(range, "range");
    261        double lm = range.getLength() * getLowerMargin();
    262        double um = range.getLength() * getUpperMargin();
    263        double lowerBound = range.getMin() - lm;
    264        double upperBound = range.getMax() + um;
    265        if (this.autoRangeIncludesZero) {
    266            lowerBound = Math.min(lowerBound, 0.0);
    267            upperBound = Math.max(upperBound, 0.0);
    268        }
    269        // does zero fall in the margins?
    270        if (this.autoRangeStickyZero) {
    271            if (0.0 <= range.getMin() && 0.0 > lowerBound) {
    272                lowerBound = 0.0;
    273            }
    274            if (0.0 >= range.getMax() && 0.0 < upperBound) {
    275                upperBound = 0.0;
    276            }
    277        }
    278        if ((upperBound - lowerBound) < getMinAutoRangeLength()) {
    279            double adj = (getMinAutoRangeLength() - (upperBound - lowerBound))
    280                    / 2.0;
    281            lowerBound -= adj;
    282            upperBound += adj;
    283        }
    284        return new Range(lowerBound, upperBound);
    285    }
    286
    287    /**
    288     * Draws the axis to the supplied graphics target ({@code g2}, with the
    289     * specified starting and ending points for the line.  This method is used
    290     * internally, you should not need to call it directly.
    291     *
    292     * @param g2  the graphics target ({@code null} not permitted).
    293     * @param pt0  the starting point ({@code null} not permitted).
    294     * @param pt1  the ending point ({@code null} not permitted).
    295     * @param opposingPt  an opposing point (to determine which side of the
    296     *     axis line the labels should appear, {@code null} not permitted).
    297     * @param tickData  tick details ({@code null} not permitted).
    298     * @param info  an object to be populated with rendering info
    299     *     ({@code null} permitted).
    300     * @param hinting  perform element hinting?
    301     */
    302    @Override
    303    public void draw(Graphics2D g2, Point2D pt0, Point2D pt1,
    304            Point2D opposingPt, List<TickData> tickData, RenderingInfo info,
    305            boolean hinting) {
    306
    307        if (!isVisible()) {
    308            return;
    309        }
    310        if (pt0.equals(pt1)) {
    311            return;
    312        }
    313
    314        // draw a line for the axis
    315        g2.setStroke(getLineStroke());
    316        g2.setPaint(getLineColor());
    317        Line2D axisLine = new Line2D.Float(pt0, pt1);
    318        g2.draw(axisLine);
    319
    320        // draw the tick marks and labels
    321        g2.setFont(getTickLabelFont());
    322        // we track the max width or height of the labels to know how far to
    323        // offset the axis label when we draw it later
    324        double maxTickLabelDim = 0.0;
    325        if (getTickLabelOrientation().equals(LabelOrientation.PARALLEL)) {
    326            LineMetrics lm = g2.getFontMetrics().getLineMetrics("123", g2);
    327            maxTickLabelDim = lm.getHeight();
    328        }
    329        double tickMarkLength = getTickMarkLength();
    330        double tickLabelOffset = getTickLabelOffset();
    331        g2.setPaint(getTickMarkPaint());
    332        g2.setStroke(getTickMarkStroke());
    333        for (TickData t : tickData) {
    334            if (tickMarkLength > 0.0) {
    335                Line2D tickLine = Utils2D.createPerpendicularLine(axisLine,
    336                       t.getAnchorPt(), tickMarkLength, opposingPt);
    337                g2.draw(tickLine);
    338            }
    339            String tickLabel = this.tickLabelFormatter.format(t.getDataValue());
    340            if (getTickLabelOrientation().equals(
    341                    LabelOrientation.PERPENDICULAR)) {
    342                maxTickLabelDim = Math.max(maxTickLabelDim,
    343                        g2.getFontMetrics().stringWidth(tickLabel));
    344            }
    345        }
    346
    347        if (getTickLabelsVisible()) {
    348            g2.setPaint(getTickLabelColor());
    349            if (getTickLabelOrientation().equals(
    350                    LabelOrientation.PERPENDICULAR)) {
    351                drawPerpendicularTickLabels(g2, axisLine, opposingPt, tickData,
    352                        info, hinting);
    353            } else {
    354                drawParallelTickLabels(g2, axisLine, opposingPt, tickData,
    355                        info, hinting);
    356            }
    357        } else {
    358            maxTickLabelDim = 0.0;
    359        }
    360
    361        // draw the axis label (if any)...
    362        if (getLabel() != null) {
    363            Shape labelBounds = drawAxisLabel(getLabel(), g2, axisLine,
    364                    opposingPt, maxTickLabelDim + tickMarkLength
    365                    + tickLabelOffset + getLabelOffset(), info, hinting);
    366        }
    367    }
    368
    369    /**
    370     * Draws tick labels parallel to the axis.
    371     *
    372     * @param g2  the graphics target ({@code null} not permitted).
    373     * @param axisLine  the axis line ({@code null} not permitted).
    374     * @param opposingPt  an opposing point (to determine on which side the
    375     *     labels appear, {@code null} not permitted).
    376     * @param tickData  the tick data ({@code null} not permitted).
    377     * @param info  if not {@code null} this object will be updated with
    378     *     {@link RenderedElement} instances for each of the tick labels.
    379     */
    380    private void drawParallelTickLabels(Graphics2D g2, Line2D axisLine,
    381            Point2D opposingPt, List<TickData> tickData, RenderingInfo info,
    382            boolean hinting) {
    383
    384        g2.setFont(getTickLabelFont());
    385        double halfAscent = g2.getFontMetrics().getAscent() / 2.0;
    386        for (TickData t : tickData) {
    387            Line2D perpLine = Utils2D.createPerpendicularLine(axisLine,
    388                    t.getAnchorPt(), getTickMarkLength()
    389                    + getTickLabelOffset() + halfAscent, opposingPt);
    390            double axisTheta = Utils2D.calculateTheta(axisLine);
    391            TextAnchor textAnchor = TextAnchor.CENTER;
    392            if (axisTheta >= Math.PI / 2.0) {
    393                axisTheta = axisTheta - Math.PI;
    394            } else if (axisTheta <= -Math.PI / 2) {
    395                axisTheta = axisTheta + Math.PI;
    396            }
    397            String tickLabel = this.tickLabelFormatter.format(
    398                    t.getDataValue());
    399            if (hinting) {
    400                Map<String, String> m = new HashMap<>();
    401                m.put("ref", "{\"type\": \"valueTickLabel\", \"axis\": \""
    402                        + axisStr() + "\", \"value\": \""
    403                        + t.getDataValue() + "\"}");
    404                g2.setRenderingHint(Chart3DHints.KEY_BEGIN_ELEMENT, m);
    405            }
    406            Shape bounds = TextUtils.drawRotatedString(tickLabel, g2,
    407                    (float) perpLine.getX2(), (float) perpLine.getY2(),
    408                    textAnchor, axisTheta, textAnchor);
    409            if (hinting) {
    410                g2.setRenderingHint(Chart3DHints.KEY_END_ELEMENT, true);
    411            }
    412            if (info != null) {
    413                RenderedElement tickLabelElement = new RenderedElement(
    414                        InteractiveElementType.VALUE_AXIS_TICK_LABEL, bounds);
    415                tickLabelElement.setProperty("axis", axisStr());
    416                tickLabelElement.setProperty("value",  t.getDataValue());
    417                info.addOffsetElement(tickLabelElement);
    418            }
    419        }
    420    }
    421
    422    /**
    423     * Draws tick labels perpendicular to the axis.
    424     *
    425     * @param g2  the graphics target ({@code null} not permitted).
    426     * @param axisLine  the axis line ({@code null} not permitted).
    427     * @param opposingPt  an opposing point (to determine on which side the
    428     *     labels appear, {@code null} not permitted).
    429     * @param tickData  the tick data ({@code null} not permitted).
    430     * @param info  if not {@code null} this object will be updated with
    431     *     {@link RenderedElement} instances for each of the tick labels.
    432     */
    433    private void drawPerpendicularTickLabels(Graphics2D g2, Line2D axisLine,
    434            Point2D opposingPt, List<TickData> tickData, RenderingInfo info,
    435            boolean hinting) {
    436        for (TickData t : tickData) {
    437            double theta = Utils2D.calculateTheta(axisLine);
    438            double thetaAdj = theta + Math.PI / 2.0;
    439            if (thetaAdj < -Math.PI / 2.0) {
    440                thetaAdj = thetaAdj + Math.PI;
    441            }
    442            if (thetaAdj > Math.PI / 2.0) {
    443                thetaAdj = thetaAdj - Math.PI;
    444            }
    445
    446            Line2D perpLine = Utils2D.createPerpendicularLine(axisLine,
    447                    t.getAnchorPt(), getTickMarkLength() + getTickLabelOffset(),
    448                    opposingPt);
    449            double perpTheta = Utils2D.calculateTheta(perpLine);
    450            TextAnchor textAnchor = TextAnchor.CENTER_LEFT;
    451            if (Math.abs(perpTheta) > Math.PI / 2.0) {
    452                textAnchor = TextAnchor.CENTER_RIGHT;
    453            }
    454            String tickLabel = this.tickLabelFormatter.format(
    455                    t.getDataValue());
    456            if (hinting) {
    457                Map<String, String> m = new HashMap<>();
    458                m.put("ref", "{\"type\": \"valueTickLabel\", \"axis\": \""
    459                        + axisStr() + "\", \"value\": \""
    460                        + t.getDataValue() + "\"}");
    461                g2.setRenderingHint(Chart3DHints.KEY_BEGIN_ELEMENT, m);
    462            }
    463            Shape bounds = TextUtils.drawRotatedString(tickLabel, g2,
    464                    (float) perpLine.getX2(), (float) perpLine.getY2(),
    465                    textAnchor, thetaAdj, textAnchor);
    466            if (hinting) {
    467                g2.setRenderingHint(Chart3DHints.KEY_END_ELEMENT, true);
    468            }
    469            if (info != null) {
    470                RenderedElement tickLabelElement = new RenderedElement(
    471                        InteractiveElementType.VALUE_AXIS_TICK_LABEL, bounds);
    472                tickLabelElement.setProperty("axis", axisStr());
    473                tickLabelElement.setProperty("value", t.getDataValue());
    474                info.addOffsetElement(tickLabelElement);
    475            }
    476        }
    477    }
    478
    479    /**
    480     * Converts a data value to world coordinates, taking into account the
    481     * current axis range (assumes the world axis is zero-based and has the
    482     * specified length).
    483     *
    484     * @param value  the data value (in axis units).
    485     * @param length  the length of the (zero based) world axis.
    486     *
    487     * @return A world coordinate.
    488     */
    489    @Override
    490    public double translateToWorld(double value, double length) {
    491        double p = getRange().percent(value, isInverted());
    492        return length * p;
    493    }
    494
    495    /**
    496     * Selects a tick size that is appropriate for drawing the axis from
    497     * {@code pt0} to {@code pt1}.
    498     *
    499     * @param g2  the graphics target ({@code null} not permitted).
    500     * @param pt0  the starting point for the axis.
    501     * @param pt1  the ending point for the axis.
    502     * @param opposingPt  a point on the opposite side of the line from where
    503     *     the labels should be drawn.
    504     */
    505    @Override
    506    public double selectTick(Graphics2D g2, Point2D pt0, Point2D pt1,
    507            Point2D opposingPt) {
    508
    509        if (this.tickSelector == null) {
    510            return this.tickSize;
    511        }
    512        g2.setFont(getTickLabelFont());
    513        FontMetrics fm = g2.getFontMetrics(getTickLabelFont());
    514        double length = pt0.distance(pt1);
    515        LabelOrientation orientation = getTickLabelOrientation();
    516        if (orientation.equals(LabelOrientation.PERPENDICULAR)) {
    517            // based on the font height, we can determine roughly how many tick
    518            // labels will fit in the length available
    519            double height = fm.getHeight();
    520            // the tickLabelFactor allows some control over how dense the labels
    521            // will be
    522            int maxTicks = (int) (length / (height * getTickLabelFactor()));
    523            if (maxTicks > 2 && this.tickSelector != null) {
    524                double rangeLength = getRange().getLength();
    525                this.tickSelector.select(rangeLength / 2.0);
    526                // step through until we have too many ticks OR we run out of
    527                // tick sizes
    528                int tickCount = (int) (rangeLength
    529                        / this.tickSelector.getCurrentTickSize());
    530                while (tickCount < maxTicks) {
    531                    this.tickSelector.previous();
    532                    tickCount = (int) (rangeLength
    533                            / this.tickSelector.getCurrentTickSize());
    534                }
    535                this.tickSelector.next();
    536                this.tickSize = this.tickSelector.getCurrentTickSize();
    537                // TFE, 20180911: don't overwrite any formatter explicitly set
    538                if (DEFAULT_TICK_LABEL_FORMATTER.equals(this.tickLabelFormatter)) {
    539                    this.tickLabelFormatter
    540                            = this.tickSelector.getCurrentTickLabelFormat();
    541                }
    542            } else {
    543                this.tickSize = Double.NaN;
    544            }
    545        } else if (orientation.equals(LabelOrientation.PARALLEL)) {
    546            // choose a unit that is at least as large as the length of the axis
    547            this.tickSelector.select(getRange().getLength());
    548            boolean done = false;
    549            while (!done) {
    550                if (this.tickSelector.previous()) {
    551                    // estimate the label widths, and do they overlap?
    552                    Format f = this.tickSelector.getCurrentTickLabelFormat();
    553                    String s0 = f.format(this.range.getMin());
    554                    String s1 = f.format(this.range.getMax());
    555                    double w0 = fm.stringWidth(s0);
    556                    double w1 = fm.stringWidth(s1);
    557                    double w = Math.max(w0, w1);
    558                    int n = (int) (length / (w * this.getTickLabelFactor()));
    559                    if (n < getRange().getLength()
    560                            / tickSelector.getCurrentTickSize()) {
    561                        tickSelector.next();
    562                        done = true;
    563                    }
    564                } else {
    565                    done = true;
    566                }
    567            }
    568            this.tickSize = this.tickSelector.getCurrentTickSize();
    569            // TFE, 20180911: don't overwrite any formatter explicitly set
    570            if (DEFAULT_TICK_LABEL_FORMATTER.equals(this.tickLabelFormatter)) {
    571                this.tickLabelFormatter
    572                        = this.tickSelector.getCurrentTickLabelFormat();
    573            }
    574        }
    575        return this.tickSize;
    576    }
    577
    578    /**
    579     * Generates a list of tick data items for the specified tick unit.  This
    580     * data will be passed to the 3D engine and will be updated with a 2D
    581     * projection that can later be used to write the axis tick labels in the
    582     * appropriate places.
    583     * <br><br>
    584     * If {@code tickUnit} is {@code Double.NaN}, then tick data is
    585     * generated for just the bounds of the axis.
    586     *
    587     * @param tickUnit  the tick unit.
    588     *
    589     * @return A list of tick data (never {@code null}).
    590     */
    591    @Override
    592    public List<TickData> generateTickData(double tickUnit) {
    593        List<TickData> result = new ArrayList<>();
    594        if (Double.isNaN(tickUnit)) {
    595            result.add(new TickData(0, getRange().getMin()));
    596            result.add(new TickData(1, getRange().getMax()));
    597        } else {
    598            double x = tickUnit * Math.ceil(this.range.getMin() / tickUnit);
    599            while (x <= this.range.getMax()) {
    600                result.add(new TickData(this.range.percent(x, isInverted()),
    601                        x));
    602                x += tickUnit;
    603            }
    604        }
    605        return result;
    606    }
    607
    608    /**
    609     * Tests this instance for equality with an arbitrary object.
    610     *
    611     * @param obj  the object to test against ({@code null} permitted).
    612     *
    613     * @return A boolean.
    614     */
    615    @Override
    616    public boolean equals(Object obj) {
    617        if (obj == this) {
    618            return true;
    619        }
    620        if (!(obj instanceof NumberAxis3D)) {
    621            return false;
    622        }
    623        NumberAxis3D that = (NumberAxis3D) obj;
    624        if (this.autoRangeIncludesZero != that.autoRangeIncludesZero) {
    625            return false;
    626        }
    627        if (this.autoRangeStickyZero != that.autoRangeStickyZero) {
    628            return false;
    629        }
    630        if (this.tickSize != that.tickSize) {
    631            return false;
    632        }
    633        if (!ObjectUtils.equals(this.tickSelector, that.tickSelector)) {
    634            return false;
    635        }
    636        if (!this.tickLabelFormatter.equals(that.tickLabelFormatter)) {
    637            return false;
    638        }
    639        return super.equals(obj);
    640    }
    641
    642    /**
    643     * Returns a hash code for this instance.
    644     *
    645     * @return A hash code.
    646     */
    647    @Override
    648    public int hashCode() {
    649        int hash = 3;
    650        hash = 59 * hash + (int) (Double.doubleToLongBits(this.tickSize)
    651                ^ (Double.doubleToLongBits(this.tickSize) >>> 32));
    652        hash = 59 * hash + ObjectUtils.hashCode(this.tickLabelFormatter);
    653        return hash;
    654    }
    655
    656}