    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.font.TextAttribute;
    038import java.awt.font.TextLayout;
    039import java.awt.geom.Line2D;
    040import java.awt.geom.Point2D;
    041import java.awt.geom.Rectangle2D;
    042import java.text.AttributedString;
    043import java.text.DecimalFormat;
    044import java.text.Format;
    045import java.text.NumberFormat;
    046import java.util.ArrayList;
    047import java.util.List;
    048import java.util.HashMap;
    049import java.util.Map;
    050
    051import org.jfree.chart3d.Chart3DHints;
    052import org.jfree.chart3d.data.Range;
    053import org.jfree.chart3d.graphics2d.TextAnchor;
    054import org.jfree.chart3d.graphics3d.RenderingInfo;
    055import org.jfree.chart3d.graphics3d.internal.Utils2D;
    056import org.jfree.chart3d.internal.Args;
    057import org.jfree.chart3d.internal.ObjectUtils;
    058import org.jfree.chart3d.internal.TextUtils;
    059
    060/**
    061 * A numerical axis with a logarithmic scale.
    062 * <br><br>
    063 * NOTE: This class is serializable, but the serialization format is subject
    064 * to change in future releases and should not be relied upon for persisting
    065 * instances of this class.
    066 *
    067 * @since 1.2
    068 */
    069@SuppressWarnings("serial")
    070public class LogAxis3D extends AbstractValueAxis3D implements ValueAxis3D {
    071
    072    /** The default value for the smallest value attribute. */
    073    public static final double DEFAULT_SMALLEST_VALUE = 1E-100;
    074
    075    /** The logarithm base. */
    076    private double base = 10.0;
    077
    078    /** The logarithm of the base value - cached for performance. */
    079    private double baseLog;
    080
    081    /** The logarithms of the current axis range. */
    082    private Range logRange;
    083
    084    /**
    085     * The smallest value for the axis.  In general, only positive values
    086     * can be plotted against a log axis but to simplify the generation of
    087     * bar charts (where the base of the bars is typically at 0.0) the axis
    088     * will return {@code smallestValue} as the translated value for 0.0.
    089     * It is important to make sure there are no real data values smaller
    090     * than this value.
    091     */
    092    private double smallestValue;
    093
    094    /**
    095     * The symbol used to represent the log base on the tick labels.  If this
    096     * is {@code null} the numerical value will be displayed.
    097     */
    098    private String baseSymbol;
    099
    100    /**
    101     * The number formatter for the base value.
    102     */
    103    private NumberFormat baseFormatter = new DecimalFormat("0");
    104
    105    /**
    106     * The tick selector (if not {@code null}, then auto-tick selection is
    107     * used).
    108     */
    109    private TickSelector tickSelector = new NumberTickSelector();
    110
    111    /**
    112     * The tick size.  If the tickSelector is not {@code null} then it is
    113     * used to auto-select an appropriate tick size and format.
    114     */
    115    private double tickSize = 1.0;
    116
    117    /** The tick formatter (never {@code null}). */
    118    private Format tickLabelFormatter = new DecimalFormat("0.0");
    119
    120    /**
    121     * Creates a new log axis with a default base of 10.
    122     *
    123     * @param label  the axis label ({@code null} permitted).
    124     */
    125    public LogAxis3D(String label) {
    126        super(label, new Range(DEFAULT_SMALLEST_VALUE, 1.0));
    127        this.base = 10.0;
    128        this.baseLog = Math.log(this.base);
    129        this.logRange = new Range(calculateLog(DEFAULT_SMALLEST_VALUE),
    130                calculateLog(1.0));
    131        this.smallestValue = DEFAULT_SMALLEST_VALUE;
    132    }
    133
    134    /**
    135     * Returns the logarithmic base value.  The default value is {@code 10}.
    136     *
    137     * @return The logarithmic base value.
    138     */
    139    public double getBase() {
    140        return this.base;
    141    }
    142
    143    /**
    144     * Sets the logarithmic base value and sends an {@code Axis3DChangeEvent}
    145     * to all registered listeners.
    146     *
    147     * @param base  the base value.
    148     */
    149    public void setBase(double base) {
    150        this.base = base;
    151        this.baseLog = Math.log(base);
    152        fireChangeEvent(true);
    153    }
    154
    155    /**
    156     * Returns the base symbol, used in tick labels for the axis.  A typical
    157     * value would be "e" when using a natural logarithm scale.  If this is
    158     * {@code null}, the tick labels will display the numerical base value.
    159     * The default value is {@code null}.
    160     *
    161     * @return The base symbol (possibly {@code null}).
    162     */
    163    public String getBaseSymbol() {
    164        return this.baseSymbol;
    165    }
    166
    167    /**
    168     * Sets the base symbol and sends an {@code Axis3DChangeEvent} to all
    169     * registered listeners.  If you set this to {@code null}, the tick labels
    170     * will display a numerical representation of the base value.
    171     *
    172     * @param symbol  the base symbol ({@code null} permitted).
    173     */
    174    public void setBaseSymbol(String symbol) {
    175        this.baseSymbol = symbol;
    176        fireChangeEvent(false);
    177    }
    178
    179    /**
    180     * Returns the formatter used for the log base value when it is displayed
    181     * in tick labels.  The default value is {@code NumberFormat("0")}.
    182     *
    183     * @return The base formatter (never {@code null}).
    184     */
    185    public NumberFormat getBaseFormatter() {
    186        return this.baseFormatter;
    187    }
    188
    189    /**
    190     * Sets the formatter for the log base value and sends an
    191     * {@code Axis3DChangeEvent} to all registered listeners.
    192     *
    193     * @param formatter  the formatter ({@code null} not permitted).
    194     */
    195    public void setBaseFormatter(NumberFormat formatter) {
    196        Args.nullNotPermitted(formatter, "formatter");
    197        this.baseFormatter = formatter;
    198        fireChangeEvent(false);
    199    }
    200
    201    /**
    202     * Returns the smallest positive data value that will be represented on
    203     * the axis.  This will be used as the lower bound for the axis if the
    204     * data range contains any value from {@code 0.0} up to this value.
    205     *
    206     * @return The smallest value.
    207     */
    208    public double getSmallestValue() {
    209        return this.smallestValue;
    210    }
    211
    212    /**
    213     * Sets the smallest positive data value that will be represented on the
    214     * axis and sends an {@code Axis3DChangeEvent} to all registered listeners.
    215     *
    216     * @param smallestValue  the value (must be positive).
    217     */
    218    public void setSmallestValue(double smallestValue) {
    219        Args.positiveRequired(smallestValue, "smallestValue");
    220        this.smallestValue = smallestValue;
    221        fireChangeEvent(true);
    222    }
    223
    224    /**
    225     * Returns the tick selector for the axis.
    226     *
    227     * @return The tick selector (possibly {@code null}).
    228     */
    229    public TickSelector getTickSelector() {
    230        return this.tickSelector;
    231    }
    232
    233    /**
    234     * Sets the tick selector and sends an {@code Axis3DChangeEvent} to all
    235     * registered listeners.
    236     *
    237     * @param selector  the selector ({@code null} permitted).
    238     */
    239    public void setTickSelector(TickSelector selector) {
    240        this.tickSelector = selector;
    241        fireChangeEvent(false);
    242    }
    243
    244    /**
    245     * Returns the tick size to be used when the tick selector is
    246     * {@code null}.
    247     *
    248     * @return The tick size.
    249     */
    250    public double getTickSize() {
    251        return this.tickSize;
    252    }
    253
    254    /**
    255     * Sets the tick size and sends an {@code Axis3DChangeEvent} to all
    256     * registered listeners.
    257     *
    258     * @param tickSize  the new tick size.
    259     */
    260    public void setTickSize(double tickSize) {
    261        this.tickSize = tickSize;
    262        fireChangeEvent(false);
    263    }
    264
    265    /**
    266     * Returns the tick label formatter.  The default value is
    267     * {@code DecimalFormat("0.0")}.
    268     *
    269     * @return The tick label formatter (never {@code null}).
    270     */
    271    public Format getTickLabelFormatter() {
    272        return this.tickLabelFormatter;
    273    }
    274
    275    /**
    276     * Sets the formatter for the tick labels and sends an
    277     * {@code Axis3DChangeEvent} to all registered listeners.
    278     *
    279     * @param formatter  the formatter ({@code null} not permitted).
    280     */
    281    public void setTickLabelFormatter(Format formatter) {
    282        Args.nullNotPermitted(formatter, "formatter");
    283        this.tickLabelFormatter = formatter;
    284        fireChangeEvent(false);
    285    }
    286
    287    /**
    288     * Sets the range for the axis.  This method is overridden to check that
    289     * the range does not contain negative values, and to update the log values
    290     * for the range.
    291     *
    292     * @param range  the range ({@code nul} not permitted).
    293     */
    294    @Override
    295    public void setRange(Range range) {
    296        Args.nullNotPermitted(range, "range");
    297        this.range = new Range(Math.max(range.getMin(), this.smallestValue),
    298                range.getMax());
    299        this.logRange = new Range(calculateLog(this.range.getMin()),
    300                calculateLog(this.range.getMax()));
    301        fireChangeEvent(true);
    302    }
    303
    304    /**
    305     * Sets the range for the axis.  This method is overridden to check that
    306     * the range does not contain negative values, and to update the log values
    307     * for the range.
    308     *
    309     * @param min  the lower bound for the range.
    310     * @param max  the upper bound for the range.
    311     */
    312    @Override
    313    public void setRange(double min, double max) {
    314        Args.negativeNotPermitted(min, "min");
    315        this.range = new Range(Math.max(min, this.smallestValue), max);
    316        this.logRange = new Range(calculateLog(this.range.getMin()),
    317                calculateLog(this.range.getMax()));
    318        fireChangeEvent(true);
    319    }
    320
    321    @Override
    322    protected void updateRange(Range range) {
    323        this.range = range;
    324        this.logRange = new Range(calculateLog(this.range.getMin()),
    325                calculateLog(this.range.getMax()));
    326    }
    327
    328    /**
    329     * Calculates the log of the given {@code value}, using the current base.
    330     *
    331     * @param value  the value (negatives not permitted).
    332     *
    333     * @return The log of the given value.
    334     *
    335     * @see #calculateValue(double)
    336     * @see #getBase()
    337     */
    338    public final double calculateLog(double value) {
    339        return Math.log(value) / this.baseLog;
    340    }
    341
    342    /**
    343     * Calculates the value from a given log value.
    344     *
    345     * @param log  the log value.
    346     *
    347     * @return The value with the given log.
    348     *
    349     * @see #calculateLog(double)
    350     * @see #getBase()
    351     */
    352    public final double calculateValue(double log) {
    353        return Math.pow(this.base, log);
    354    }
    355
    356    /**
    357     * Translates a data value to a world coordinate, assuming that the axis
    358     * begins at the origin and has the specified length.
    359     *
    360     * @param value  the data value.
    361     * @param length  the axis length in world coordinates.
    362     *
    363     * @return The world coordinate of this data value on the axis.
    364     */
    365    @Override
    366    public double translateToWorld(double value, double length) {
    367        double logv = calculateLog(value);
    368        double percent = this.logRange.percent(logv);
    369        if (isInverted()) {
    370            percent = 1.0 - percent;
    371        }
    372        return percent * length;
    373    }
    374
    375    /**
    376     * Draws the axis.
    377     *
    378     * @param g2  the graphics target ({@code null} not permitted).
    379     * @param startPt  the starting point.
    380     * @param endPt  the ending point.
    381     * @param opposingPt  an opposing point (labels will be on the other side
    382     *     of the line).
    383     * @param tickData  the tick data (including anchor points calculated by
    384     *     the 3D engine).
    385     * @param info  an object to be populated with rendering info
    386     *     ({@code null} permitted).
    387     * @param hinting  perform element hinting?
    388     */
    389    @Override
    390    public void draw(Graphics2D g2, Point2D startPt, Point2D endPt,
    391            Point2D opposingPt, List<TickData> tickData, RenderingInfo info,
    392            boolean hinting) {
    393
    394        if (!isVisible()) {
    395            return;
    396        }
    397
    398        // draw a line for the axis
    399        g2.setStroke(getLineStroke());
    400        g2.setPaint(getLineColor());
    401        Line2D axisLine = new Line2D.Float(startPt, endPt);
    402        g2.draw(axisLine);
    403
    404        // draw the tick marks and labels
    405        double tickMarkLength = getTickMarkLength();
    406        double tickLabelOffset = getTickLabelOffset();
    407        g2.setPaint(getTickMarkPaint());
    408        g2.setStroke(getTickMarkStroke());
    409        for (TickData t : tickData) {
    410            if (tickMarkLength > 0.0) {
    411                Line2D tickLine = Utils2D.createPerpendicularLine(axisLine,
    412                       t.getAnchorPt(), tickMarkLength, opposingPt);
    413                g2.draw(tickLine);
    414            }
    415        }
    416
    417        double maxTickLabelDim = 0.0;
    418        if (getTickLabelsVisible()) {
    419            g2.setFont(getTickLabelFont());
    420            g2.setPaint(getTickLabelColor());
    421            LabelOrientation orientation = getTickLabelOrientation();
    422            if (orientation.equals(LabelOrientation.PERPENDICULAR)) {
    423                maxTickLabelDim = drawPerpendicularTickLabels(g2, axisLine,
    424                        opposingPt, tickData, hinting);
    425            } else if (orientation.equals(LabelOrientation.PARALLEL)) {
    426                maxTickLabelDim = g2.getFontMetrics().getHeight();
    427                double adj = g2.getFontMetrics().getAscent() / 2.0;
    428                drawParallelTickLabels(g2, axisLine, opposingPt, tickData, adj,
    429                        hinting);
    430            }
    431        }
    432
    433        // draw the axis label (if any)...
    434        if (getLabel() != null) {
    435            /* Shape labelBounds = */drawAxisLabel(getLabel(), g2, axisLine,
    436                    opposingPt, maxTickLabelDim + tickMarkLength
    437                    + tickLabelOffset + getLabelOffset(), info, hinting);
    438        }
    439    }
    440
    441    private double drawPerpendicularTickLabels(Graphics2D g2, Line2D axisLine,
    442            Point2D opposingPt, List<TickData> tickData, boolean hinting) {
    443        double result = 0.0;
    444        for (TickData t : tickData) {
    445            double theta = Utils2D.calculateTheta(axisLine);
    446            double thetaAdj = theta + Math.PI / 2.0;
    447            if (thetaAdj < -Math.PI / 2.0) {
    448                thetaAdj = thetaAdj + Math.PI;
    449            }
    450            if (thetaAdj > Math.PI / 2.0) {
    451                thetaAdj = thetaAdj - Math.PI;
    452            }
    453            Line2D perpLine = Utils2D.createPerpendicularLine(axisLine,
    454                    t.getAnchorPt(), getTickMarkLength()
    455                    + getTickLabelOffset(), opposingPt);
    456            double perpTheta = Utils2D.calculateTheta(perpLine);
    457            TextAnchor textAnchor = TextAnchor.CENTER_LEFT;
    458            if (Math.abs(perpTheta) > Math.PI / 2.0) {
    459                textAnchor = TextAnchor.CENTER_RIGHT;
    460            }
    461            double logy = calculateLog(t.getDataValue());
    462            AttributedString as = createTickLabelAttributedString(logy,
    463                    this.tickLabelFormatter);
    464            Rectangle2D nonRotatedBounds = new Rectangle2D.Double();
    465            if (hinting) {
    466                Map<String, String> m = new HashMap<>();
    467                m.put("ref", "{\"type\": \"valueTickLabel\", \"axis\": "
    468                        + axisStr() + ", \"value\": \""
    469                        + t.getDataValue() + "\"}");
    470                g2.setRenderingHint(Chart3DHints.KEY_BEGIN_ELEMENT, m);
    471            }
    472            TextUtils.drawRotatedString(as, g2,
    473                    (float) perpLine.getX2(), (float) perpLine.getY2(),
    474                    textAnchor, thetaAdj, textAnchor, nonRotatedBounds);
    475            if (hinting) {
    476                g2.setRenderingHint(Chart3DHints.KEY_END_ELEMENT, true);
    477            }
    478            result = Math.max(result, nonRotatedBounds.getWidth());
    479        }
    480        return result;
    481    }
    482
    483    private void drawParallelTickLabels(Graphics2D g2, Line2D axisLine,
    484            Point2D opposingPt, List<TickData> tickData, double adj,
    485            boolean hinting) {
    486
    487        for (TickData t : tickData) {
    488            double theta = Utils2D.calculateTheta(axisLine);
    489            TextAnchor anchor = TextAnchor.CENTER;
    490            if (theta < -Math.PI / 2.0) {
    491                theta = theta + Math.PI;
    492                anchor = TextAnchor.CENTER;
    493            }
    494            if (theta > Math.PI / 2.0) {
    495                theta = theta - Math.PI;
    496                anchor = TextAnchor.CENTER;
    497            }
    498            Line2D perpLine = Utils2D.createPerpendicularLine(axisLine,
    499                    t.getAnchorPt(), getTickMarkLength()
    500                    + getTickLabelOffset() + adj, opposingPt);
    501            double logy = calculateLog(t.getDataValue());
    502            AttributedString as = createTickLabelAttributedString(logy,
    503                    this.tickSelector.getCurrentTickLabelFormat());
    504            if (hinting) {
    505                Map<String, String> m = new HashMap<>();
    506                m.put("ref", "{\"type\": \"valueTickLabel\", \"axis\": "
    507                        + axisStr() + ", \"value\": \""
    508                        + t.getDataValue() + "\"}");
    509                g2.setRenderingHint(Chart3DHints.KEY_BEGIN_ELEMENT, m);
    510            }
    511            TextUtils.drawRotatedString(as, g2,
    512                    (float) perpLine.getX2(), (float) perpLine.getY2(),
    513                    anchor, theta, anchor, null);
    514            if (hinting) {
    515                g2.setRenderingHint(Chart3DHints.KEY_END_ELEMENT, true);
    516            }
    517        }
    518    }
    519
    520    private AttributedString createTickLabelAttributedString(double logy,
    521            Format exponentFormatter) {
    522        String baseStr = this.baseSymbol;
    523        if (baseStr == null) {
    524            baseStr = this.baseFormatter.format(this.base);
    525        }
    526        String exponentStr = exponentFormatter.format(logy);
    527        AttributedString as = new AttributedString(baseStr + exponentStr);
    528        as.addAttributes(getTickLabelFont().getAttributes(), 0, (baseStr
    529                + exponentStr).length());
    530        as.addAttribute(TextAttribute.SUPERSCRIPT,
    531                TextAttribute.SUPERSCRIPT_SUPER, baseStr.length(),
    532                baseStr.length() + exponentStr.length());
    533        return as;
    534    }
    535
    536    /**
    537     * Adjusts the range by adding the lower and upper margins on the
    538     * logarithmic range.
    539     *
    540     * @param range  the range ({@code nul} not permitted).
    541     *
    542     * @return The adjusted range.
    543     */
    544    @Override
    545    protected Range adjustedDataRange(Range range) {
    546        Args.nullNotPermitted(range, "range");
    547        double logmin = calculateLog(Math.max(range.getMin(),
    548                this.smallestValue));
    549        double logmax = calculateLog(range.getMax());
    550        double length = logmax - logmin;
    551        double lm = length * getLowerMargin();
    552        double um = length * getUpperMargin();
    553        double lowerBound = calculateValue(logmin - lm);
    554        double upperBound = calculateValue(logmax + um);
    555        return new Range(lowerBound, upperBound);
    556    }
    557
    558    /**
    559     * Selects a standard tick unit on the logarithmic range.
    560     *
    561     * @param g2  the graphics target ({@code null} not permitted).
    562     * @param pt0  the starting point.
    563     * @param pt1  the ending point.
    564     * @param opposingPt  an opposing point.
    565     *
    566     * @return The tick unit (log increment).
    567     */
    568    @Override
    569    public double selectTick(Graphics2D g2, Point2D pt0, Point2D pt1,
    570            Point2D opposingPt) {
    571
    572        if (this.tickSelector == null) {
    573            return this.tickSize;
    574        }
    575        g2.setFont(getTickLabelFont());
    576        FontMetrics fm = g2.getFontMetrics();
    577        double length = pt0.distance(pt1);
    578        double rangeLength = this.logRange.getLength();
    579
    580        LabelOrientation orientation = getTickLabelOrientation();
    581        if (orientation.equals(LabelOrientation.PERPENDICULAR)) {
    582            // based on the font height, we can determine roughly how many tick
    583            // labels will fit in the length available
    584            int height = fm.getHeight();
    585            // the tickLabelFactor allows some control over how dense the labels
    586            // will be
    587            int maxTicks = (int) (length / (height * getTickLabelFactor()));
    588            if (maxTicks > 2 && this.tickSelector != null) {
    589                this.tickSelector.select(rangeLength / 2.0);
    590                // step through until we have too many ticks OR we run out of
    591                // tick sizes
    592                int tickCount = (int) (rangeLength
    593                        / this.tickSelector.getCurrentTickSize());
    594                while (tickCount < maxTicks) {
    595                    this.tickSelector.previous();
    596                    tickCount = (int) (rangeLength
    597                            / this.tickSelector.getCurrentTickSize());
    598                }
    599                this.tickSelector.next();
    600                this.tickSize = this.tickSelector.getCurrentTickSize();
    601                this.tickLabelFormatter
    602                        = this.tickSelector.getCurrentTickLabelFormat();
    603            } else {
    604                this.tickSize = Double.NaN;
    605            }
    606        } else if (orientation.equals(LabelOrientation.PARALLEL)) {
    607            // choose a unit that is at least as large as the length of the axis
    608            this.tickSelector.select(rangeLength);
    609            boolean done = false;
    610            while (!done) {
    611                if (this.tickSelector.previous()) {
    612                    // estimate the label widths, and do they overlap?
    613                    AttributedString s0 = createTickLabelAttributedString(
    614                            this.logRange.getMax() + this.logRange.getMin(),
    615                            this.tickSelector.getCurrentTickLabelFormat());
    616                    TextLayout layout0 = new TextLayout(s0.getIterator(),
    617                            g2.getFontRenderContext());
    618                    double w0 = layout0.getAdvance();
    619                    AttributedString s1 = createTickLabelAttributedString(
    620                            this.logRange.getMax() + this.logRange.getMin(),
    621                            this.tickSelector.getCurrentTickLabelFormat());
    622                    TextLayout layout1 = new TextLayout(s1.getIterator(),
    623                            g2.getFontRenderContext());
    624                    double w1 = layout1.getAdvance();
    625                    double w = Math.max(w0, w1);
    626                    int n = (int) (length / (w * this.getTickLabelFactor()));
    627                    if (n < rangeLength
    628                            / tickSelector.getCurrentTickSize()) {
    629                        tickSelector.next();
    630                        done = true;
    631                    }
    632                } else {
    633                    done = true;
    634                }
    635            }
    636            this.tickSize = this.tickSelector.getCurrentTickSize();
    637            this.tickLabelFormatter
    638                    = this.tickSelector.getCurrentTickLabelFormat();
    639        }
    640        return this.tickSize;
    641    }
    642
    643    /**
    644     * Generates tick data for the axis, assuming the specified tick unit
    645     * (a log increment in this case).  If the tick unit is Double.NaN then
    646     * ticks will be added for the bounds of the axis only.
    647     *
    648     * @param tickUnit  the tick unit.
    649     *
    650     * @return A list of tick data items.
    651     */
    652    @Override
    653    public List<TickData> generateTickData(double tickUnit) {
    654        List<TickData> result = new ArrayList<>();
    655        if (Double.isNaN(tickUnit)) {
    656            result.add(new TickData(0, getRange().getMin()));
    657            result.add(new TickData(1, getRange().getMax()));
    658        } else {
    659            double logx = tickUnit
    660                    * Math.ceil(this.logRange.getMin() / tickUnit);
    661            while (logx <= this.logRange.getMax()) {
    662                result.add(new TickData(this.logRange.percent(logx),
    663                        calculateValue(logx)));
    664                logx += tickUnit;
    665            }
    666        }
    667        return result;
    668    }
    669
    670    @Override
    671    public int hashCode() {
    672        int hash = 5;
    673        hash = 59 * hash + (int) (Double.doubleToLongBits(this.base)
    674                ^ (Double.doubleToLongBits(this.base) >>> 32));
    675        hash = 59 * hash + (int) (Double.doubleToLongBits(this.smallestValue)
    676                ^ (Double.doubleToLongBits(this.smallestValue) >>> 32));
    677        hash = 59 * hash + ObjectUtils.hashCode(this.baseSymbol);
    678        hash = 59 * hash + ObjectUtils.hashCode(this.baseFormatter);
    679        hash = 59 * hash + ObjectUtils.hashCode(this.tickSelector);
    680        hash = 59 * hash + (int) (Double.doubleToLongBits(this.tickSize)
    681                ^ (Double.doubleToLongBits(this.tickSize) >>> 32));
    682        hash = 59 * hash + ObjectUtils.hashCode(this.tickLabelFormatter);
    683        return hash;
    684    }
    685
    686    @Override
    687    public boolean equals(Object obj) {
    688        if (obj == null) {
    689            return false;
    690        }
    691        if (getClass() != obj.getClass()) {
    692            return false;
    693        }
    694        final LogAxis3D other = (LogAxis3D) obj;
    695        if (Double.doubleToLongBits(this.base)
    696                != Double.doubleToLongBits(other.base)) {
    697            return false;
    698        }
    699        if (Double.doubleToLongBits(this.smallestValue)
    700                != Double.doubleToLongBits(other.smallestValue)) {
    701            return false;
    702        }
    703        if (!ObjectUtils.equals(this.baseSymbol, other.baseSymbol)) {
    704            return false;
    705        }
    706        if (!ObjectUtils.equals(this.baseFormatter, other.baseFormatter)) {
    707            return false;
    708        }
    709        if (!ObjectUtils.equals(this.tickSelector, other.tickSelector)) {
    710            return false;
    711        }
    712        if (Double.doubleToLongBits(this.tickSize)
    713                != Double.doubleToLongBits(other.tickSize)) {
    714            return false;
    715        }
    716        if (!ObjectUtils.equals(this.tickLabelFormatter,
    717                other.tickLabelFormatter)) {
    718            return false;
    719        }
    720        return super.equals(obj);
    721    }
    722
    723}