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<String, String> 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<String, String> 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}