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