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}