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}