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}