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.BasicStroke; 036import java.awt.Color; 037import java.awt.Paint; 038import java.awt.Stroke; 039import java.io.IOException; 040import java.io.ObjectInputStream; 041import java.io.ObjectOutputStream; 042import java.io.Serializable; 043import java.util.ArrayList; 044import java.util.LinkedHashMap; 045import java.util.List; 046import java.util.Map; 047 048import com.orsoncharts.ChartElementVisitor; 049import com.orsoncharts.Range; 050import com.orsoncharts.data.category.CategoryDataset3D; 051import com.orsoncharts.marker.MarkerData; 052import com.orsoncharts.marker.NumberMarker; 053import com.orsoncharts.marker.RangeMarker; 054import com.orsoncharts.marker.ValueMarker; 055import com.orsoncharts.plot.CategoryPlot3D; 056import com.orsoncharts.plot.XYZPlot; 057import com.orsoncharts.util.ArgChecks; 058import com.orsoncharts.util.ObjectUtils; 059import com.orsoncharts.util.SerialUtils; 060 061/** 062 * A base class for implementing numerical axes. 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@SuppressWarnings("serial") 069public abstract class AbstractValueAxis3D extends AbstractAxis3D 070 implements ValueAxis3D, Serializable{ 071 072 /** The type of use for which the axis has been configured. */ 073 private ValueAxis3DType configuredType; 074 075 /** The axis range. */ 076 protected Range range; 077 078 private boolean inverted; 079 080 /** 081 * A flag that controls whether or not the axis range is automatically 082 * adjusted to display all of the data items in the dataset. 083 */ 084 private boolean autoAdjustRange; 085 086 /** The percentage margin to leave at the lower end of the axis. */ 087 private double lowerMargin; 088 089 /** The percentage margin to leave at the upper end of the axis. */ 090 private double upperMargin; 091 092 /** 093 * The default range to apply when there is no data in the dataset and the 094 * autoAdjustRange flag is true. A sensible default is going to depend on 095 * the context, so the user should change it as necessary. 096 */ 097 private Range defaultAutoRange; 098 099 /** 100 * The minimum length for the axis range when auto-calculated. This will 101 * be applied, for example, when the dataset contains just a single value. 102 */ 103 private double minAutoRangeLength; 104 105 /** The tick label offset (number of Java2D units). */ 106 private double tickLabelOffset; 107 108 /** The length of tick marks (in Java2D units). Can be set to 0.0. */ 109 private double tickMarkLength; 110 111 /** The tick mark stroke (never {@code null}). */ 112 private transient Stroke tickMarkStroke; 113 114 /** The tick mark paint (never {@code null}). */ 115 private transient Paint tickMarkPaint; 116 117 /** The orientation for the tick labels. */ 118 private LabelOrientation tickLabelOrientation; 119 120 /** The tick label factor (defaults to 1.4). */ 121 private double tickLabelFactor; 122 123 /** Storage for value markers for the axis (empty by default). */ 124 private Map<String, ValueMarker> valueMarkers; 125 126 /** 127 * Creates a new axis instance. 128 * 129 * @param label the axis label ({@code null} permitted). 130 * @param range the axis range ({@code null} not permitted). 131 */ 132 public AbstractValueAxis3D(String label, Range range) { 133 super(label); 134 ArgChecks.nullNotPermitted(range, "range"); 135 this.configuredType = null; 136 this.range = range; 137 this.autoAdjustRange = true; 138 this.lowerMargin = 0.05; 139 this.upperMargin = 0.05; 140 this.defaultAutoRange = new Range(0.0, 1.0); 141 this.minAutoRangeLength = 0.001; 142 this.tickLabelOffset = 5.0; 143 this.tickLabelOrientation = LabelOrientation.PARALLEL; 144 this.tickLabelFactor = 1.4; 145 this.tickMarkLength = 3.0; 146 this.tickMarkStroke = new BasicStroke(0.5f); 147 this.tickMarkPaint = Color.GRAY; 148 this.valueMarkers = new LinkedHashMap<String, ValueMarker>(); 149 } 150 151 /** 152 * Returns the configured type for the axis. 153 * 154 * @return The configured type ({@code null} if the axis has not yet 155 * been assigned to a plot). 156 * 157 * @since 1.3 158 */ 159 @Override 160 public ValueAxis3DType getConfiguredType() { 161 return this.configuredType; 162 } 163 164 /** 165 * Returns a string representing the configured type of the axis. 166 * 167 * @return A string. 168 */ 169 @Override 170 protected String axisStr() { 171 if (this.configuredType.equals(ValueAxis3DType.VALUE)) { 172 return "value"; 173 } 174 if (this.configuredType.equals(ValueAxis3DType.X)) { 175 return "x"; 176 } 177 if (this.configuredType.equals(ValueAxis3DType.Y)) { 178 return "y"; 179 } 180 if (this.configuredType.equals(ValueAxis3DType.Z)) { 181 return "z"; 182 } 183 return ""; 184 } 185 186 /** 187 * Returns the axis range. You can set the axis range manually or you can 188 * rely on the autoAdjustRange feature to set the axis range to match 189 * the data being plotted. 190 * 191 * @return the axis range (never {@code null}). 192 */ 193 @Override 194 public Range getRange() { 195 return this.range; 196 } 197 198 /** 199 * Sets the axis range (bounds) and sends an {@link Axis3DChangeEvent} to 200 * all registered listeners. 201 * 202 * @param range the new range (must have positive length and 203 * {@code null} is not permitted). 204 */ 205 @Override 206 public void setRange(Range range) { 207 ArgChecks.nullNotPermitted(range, "range"); 208 if (range.getLength() <= 0.0) { 209 throw new IllegalArgumentException( 210 "Requires a range with length > 0"); 211 } 212 this.range = range; 213 this.autoAdjustRange = false; 214 fireChangeEvent(true); 215 } 216 217 /** 218 * Updates the axis range (used by the auto-range calculation) without 219 * notifying listeners. 220 * 221 * @param range the new range. 222 */ 223 protected void updateRange(Range range) { 224 this.range = range; 225 } 226 227 /** 228 * Sets the axis range and sends an {@link Axis3DChangeEvent} to all 229 * registered listeners. 230 * 231 * @param min the lower bound for the range (requires min < max). 232 * @param max the upper bound for the range (requires max > min). 233 */ 234 @Override 235 public void setRange(double min, double max) { 236 setRange(new Range(min, max)); 237 } 238 239 /** 240 * Returns the flag that controls whether or not the axis range is 241 * automatically updated in response to dataset changes. The default 242 * value is {@code true}. 243 * 244 * @return A boolean. 245 */ 246 public boolean isAutoAdjustRange() { 247 return this.autoAdjustRange; 248 } 249 250 /** 251 * Sets the flag that controls whether or not the axis range is 252 * automatically updated in response to dataset changes, and sends an 253 * {@link Axis3DChangeEvent} to all registered listeners. 254 * 255 * @param autoAdjust the new flag value. 256 */ 257 public void setAutoAdjustRange(boolean autoAdjust) { 258 this.autoAdjustRange = autoAdjust; 259 fireChangeEvent(true); 260 } 261 262 /** 263 * Returns the size of the lower margin that is added by the auto-range 264 * calculation, as a percentage of the data range. This margin is used to 265 * prevent data items from being plotted right at the edges of the chart. 266 * The default value is {@code 0.05} (five percent). 267 * 268 * @return The lower margin. 269 */ 270 public double getLowerMargin() { 271 return this.lowerMargin; 272 } 273 274 /** 275 * Sets the size of the lower margin that will be added by the auto-range 276 * calculation and sends an {@link Axis3DChangeEvent} to all registered 277 * listeners. 278 * 279 * @param margin the margin as a percentage of the data range 280 * (0.05 = five percent). 281 * 282 * @see #setUpperMargin(double) 283 */ 284 public void setLowerMargin(double margin) { 285 this.lowerMargin = margin; 286 fireChangeEvent(true); 287 } 288 289 /** 290 * Returns the size of the upper margin that is added by the auto-range 291 * calculation, as a percentage of the data range. This margin is used to 292 * prevent data items from being plotted right at the edges of the chart. 293 * The default value is {@code 0.05} (five percent). 294 * 295 * @return The upper margin. 296 */ 297 public double getUpperMargin() { 298 return this.upperMargin; 299 } 300 301 /** 302 * Sets the size of the upper margin that will be added by the auto-range 303 * calculation and sends an {@link Axis3DChangeEvent} to all registered 304 * listeners. 305 * 306 * @param margin the margin as a percentage of the data range 307 * (0.05 = five percent). 308 * 309 * @see #setLowerMargin(double) 310 */ 311 public void setUpperMargin(double margin) { 312 this.upperMargin = margin; 313 fireChangeEvent(true); 314 } 315 316 317 /** 318 * Returns the default range used when the {@code autoAdjustRange} 319 * flag is {@code true} but the dataset contains no values. The 320 * default range is {@code (0.0 to 1.0)}, depending on the context 321 * you may want to change this. 322 * 323 * @return The default range (never {@code null}). 324 * 325 * @see #setDefaultAutoRange(com.orsoncharts.Range) 326 */ 327 public Range getDefaultAutoRange() { 328 return this.defaultAutoRange; 329 } 330 331 /** 332 * Sets the default range used when the {@code autoAdjustRange} 333 * flag is {@code true} but the dataset contains no values, and sends 334 * an {@link Axis3DChangeEvent} to all registered listeners. 335 * 336 * @param range the range ({@code null} not permitted). 337 * 338 * @see #getDefaultAutoRange() 339 */ 340 public void setDefaultAutoRange(Range range) { 341 ArgChecks.nullNotPermitted(range, "range"); 342 this.defaultAutoRange = range; 343 fireChangeEvent(true); 344 } 345 346 /** 347 * Returns the minimum length for the axis range when auto-calculated. 348 * The default value is 0.001. 349 * 350 * @return The minimum length. 351 * 352 * @since 1.4 353 */ 354 public double getMinAutoRangeLength() { 355 return this.minAutoRangeLength; 356 } 357 358 /** 359 * Sets the minimum length for the axis range when it is auto-calculated 360 * and sends a change event to all registered listeners. 361 * 362 * @param length the new minimum length. 363 * 364 * @since 1.4 365 */ 366 public void setMinAutoRangeLength(double length) { 367 ArgChecks.positiveRequired(length, "length"); 368 this.minAutoRangeLength = length; 369 fireChangeEvent(this.range.getLength() < length); 370 } 371 372 /** 373 * Returns the flag that determines whether or not the order of values on 374 * the axis is inverted. The default value is {@code false}. 375 * 376 * @return A boolean. 377 * 378 * @since 1.5 379 */ 380 @Override 381 public boolean isInverted() { 382 return this.inverted; 383 } 384 385 /** 386 * Sets the flag that determines whether or not the order of values on the 387 * axis is inverted, and sends an {@link Axis3DChangeEvent} to all 388 * registered listeners. 389 * 390 * @param inverted the new flag value. 391 * 392 * @since 1.5 393 */ 394 public void setInverted(boolean inverted) { 395 this.inverted = inverted; 396 fireChangeEvent(true); 397 } 398 399 /** 400 * Returns the orientation for the tick labels. The default value is 401 * {@link LabelOrientation#PARALLEL}. 402 * 403 * @return The orientation for the tick labels (never {@code null}). 404 * 405 * @since 1.2 406 */ 407 public LabelOrientation getTickLabelOrientation() { 408 return this.tickLabelOrientation; 409 } 410 411 /** 412 * Sets the orientation for the tick labels and sends a change event to 413 * all registered listeners. In general, {@code PARALLEL} is the 414 * best setting for X and Z axes, and {@code PERPENDICULAR} is the 415 * best setting for Y axes. 416 * 417 * @param orientation the orientation ({@code null} not permitted). 418 * 419 * @since 1.2 420 */ 421 public void setTickLabelOrientation(LabelOrientation orientation) { 422 ArgChecks.nullNotPermitted(orientation, "orientation"); 423 this.tickLabelOrientation = orientation; 424 fireChangeEvent(false); 425 } 426 427 /** 428 * Returns the tick label factor, a multiplier for the label height to 429 * determine the maximum number of tick labels that can be displayed. 430 * The default value is {@code 1.4}. 431 * 432 * @return The tick label factor. 433 */ 434 public double getTickLabelFactor() { 435 return this.tickLabelFactor; 436 } 437 438 /** 439 * Sets the tick label factor and sends an {@link Axis3DChangeEvent} 440 * to all registered listeners. This should be at least 1.0, higher values 441 * will result in larger gaps between the tick marks. 442 * 443 * @param factor the factor. 444 */ 445 public void setTickLabelFactor(double factor) { 446 this.tickLabelFactor = factor; 447 fireChangeEvent(false); 448 } 449 450 /** 451 * Returns the tick label offset, the gap between the tick marks and the 452 * tick labels (in Java2D units). The default value is {@code 5.0}. 453 * 454 * @return The tick label offset. 455 */ 456 public double getTickLabelOffset() { 457 return this.tickLabelOffset; 458 } 459 460 /** 461 * Sets the tick label offset and sends an {@link Axis3DChangeEvent} to 462 * all registered listeners. 463 * 464 * @param offset the offset. 465 */ 466 public void setTickLabelOffset(double offset) { 467 this.tickLabelOffset = offset; 468 } 469 470 /** 471 * Returns the length of the tick marks (in Java2D units). The default 472 * value is {@code 3.0}. 473 * 474 * @return The length of the tick marks. 475 */ 476 public double getTickMarkLength() { 477 return this.tickMarkLength; 478 } 479 480 /** 481 * Sets the length of the tick marks and sends an {@link Axis3DChangeEvent} 482 * to all registered listeners. You can set this to {@code 0.0} if 483 * you prefer no tick marks to be displayed on the axis. 484 * 485 * @param length the length (in Java2D units). 486 */ 487 public void setTickMarkLength(double length) { 488 this.tickMarkLength = length; 489 fireChangeEvent(false); 490 } 491 492 /** 493 * Returns the stroke used to draw the tick marks. The default value is 494 * {@code BasicStroke(0.5f)}. 495 * 496 * @return The tick mark stroke (never {@code null}). 497 */ 498 public Stroke getTickMarkStroke() { 499 return this.tickMarkStroke; 500 } 501 502 /** 503 * Sets the stroke used to draw the tick marks and sends an 504 * {@link Axis3DChangeEvent} to all registered listeners. 505 * 506 * @param stroke the stroke ({@code null} not permitted). 507 */ 508 public void setTickMarkStroke(Stroke stroke) { 509 ArgChecks.nullNotPermitted(stroke, "stroke"); 510 this.tickMarkStroke = stroke; 511 fireChangeEvent(false); 512 } 513 514 /** 515 * Returns the paint used to draw the tick marks. The default value is 516 * {@code Color.GRAY}. 517 * 518 * @return The tick mark paint (never {@code null}). 519 */ 520 public Paint getTickMarkPaint() { 521 return this.tickMarkPaint; 522 } 523 524 /** 525 * Sets the paint used to draw the tick marks and sends an 526 * {@link Axis3DChangeEvent} to all registered listeners. 527 * 528 * @param paint the paint ({@code null} not permitted). 529 */ 530 public void setTickMarkPaint(Paint paint) { 531 ArgChecks.nullNotPermitted(paint, "paint"); 532 this.tickMarkPaint = paint; 533 fireChangeEvent(false); 534 } 535 536 /** 537 * Configures the axis to be used as the value axis for the specified 538 * plot. This method is used internally, you should not need to call it 539 * directly. 540 * 541 * @param plot the plot ({@code null} not permitted). 542 */ 543 @Override 544 public void configureAsValueAxis(CategoryPlot3D plot) { 545 this.configuredType = ValueAxis3DType.VALUE; 546 if (this.autoAdjustRange) { 547 CategoryDataset3D dataset = plot.getDataset(); 548 Range valueRange = plot.getRenderer().findValueRange(dataset); 549 if (valueRange != null) { 550 updateRange(adjustedDataRange(valueRange)); 551 } else { 552 updateRange(this.defaultAutoRange); 553 } 554 } 555 } 556 557 /** 558 * Configures the axis to be used as the x-axis for the specified plot. 559 * This method is used internally, you should not need to call it 560 * directly. 561 * 562 * @param plot the plot ({@code null} not permitted). 563 */ 564 @Override 565 public void configureAsXAxis(XYZPlot plot) { 566 this.configuredType = ValueAxis3DType.X; 567 if (this.autoAdjustRange) { 568 Range xRange = plot.getRenderer().findXRange(plot.getDataset()); 569 if (xRange != null) { 570 updateRange(adjustedDataRange(xRange)); 571 } else { 572 updateRange(this.defaultAutoRange); 573 } 574 } 575 } 576 577 /** 578 * Configures the axis to be used as the y-axis for the specified plot. 579 * This method is used internally, you should not need to call it 580 * directly. 581 * 582 * @param plot the plot ({@code null} not permitted). 583 */ 584 @Override 585 public void configureAsYAxis(XYZPlot plot) { 586 this.configuredType = ValueAxis3DType.Y; 587 if (this.autoAdjustRange) { 588 Range yRange = plot.getRenderer().findYRange(plot.getDataset()); 589 if (yRange != null) { 590 updateRange(adjustedDataRange(yRange)); 591 } else { 592 updateRange(this.defaultAutoRange); 593 } 594 } 595 } 596 597 /** 598 * Configures the axis to be used as the z-axis for the specified plot. 599 * This method is used internally, you should not need to call it 600 * directly. 601 * 602 * @param plot the plot ({@code null} not permitted). 603 */ 604 @Override 605 public void configureAsZAxis(XYZPlot plot) { 606 this.configuredType = ValueAxis3DType.Z; 607 if (this.autoAdjustRange) { 608 Range zRange = plot.getRenderer().findZRange(plot.getDataset()); 609 if (zRange != null) { 610 updateRange(adjustedDataRange(zRange)); 611 } else { 612 updateRange(this.defaultAutoRange); 613 } 614 } 615 } 616 617 /** 618 * Adjusts the range by adding the lower and upper margins and taking into 619 * account any other settings. 620 * 621 * @param range the range ({@code null} not permitted). 622 * 623 * @return The adjusted range. 624 */ 625 protected abstract Range adjustedDataRange(Range range); 626 627 /** 628 * Returns the marker with the specified key, if there is one. 629 * 630 * @param key the key ({@code null} not permitted). 631 * 632 * @return The marker (possibly {@code null}). 633 * 634 * @since 1.2 635 */ 636 @Override 637 public ValueMarker getMarker(String key) { 638 return this.valueMarkers.get(key); 639 } 640 641 /** 642 * Sets the marker for the specified key and sends a change event to 643 * all registered listeners. If there is an existing marker it is replaced 644 * (the axis will no longer listen for change events on the previous 645 * marker). 646 * 647 * @param key the key that identifies the marker ({@code null} not 648 * permitted). 649 * @param marker the marker ({@code null} permitted). 650 * 651 * @since 1.2 652 */ 653 public void setMarker(String key, ValueMarker marker) { 654 ValueMarker existing = this.valueMarkers.get(key); 655 if (existing != null) { 656 existing.removeChangeListener(this); 657 } 658 this.valueMarkers.put(key, marker); 659 marker.addChangeListener(this); 660 fireChangeEvent(false); 661 } 662 663 /** 664 * Returns a new map containing the markers assigned to this axis. 665 * 666 * @return A map. 667 * 668 * @since 1.2 669 */ 670 public Map<String, ValueMarker> getMarkers() { 671 return new LinkedHashMap<String, ValueMarker>(this.valueMarkers); 672 } 673 674 /** 675 * Generates and returns a list of marker data items for the axis. 676 * 677 * @return A list of marker data items (never {@code null}). 678 */ 679 @Override 680 public List<MarkerData> generateMarkerData() { 681 List<MarkerData> result = new ArrayList<MarkerData>(); 682 Range range = getRange(); 683 for (Map.Entry<String, ValueMarker> entry 684 : this.valueMarkers.entrySet()) { 685 ValueMarker vm = entry.getValue(); 686 if (range.intersects(vm.getRange())) { 687 MarkerData markerData; 688 if (vm instanceof NumberMarker) { 689 NumberMarker nm = (NumberMarker) vm; 690 markerData = new MarkerData(entry.getKey(), 691 range.percent(nm.getValue())); 692 markerData.setLabelAnchor(nm.getLabel() != null 693 ? nm.getLabelAnchor() : null); 694 } else if (vm instanceof RangeMarker) { 695 RangeMarker rm = (RangeMarker) vm; 696 double startValue = rm.getStart().getValue(); 697 boolean startPegged = false; 698 if (!range.contains(startValue)) { 699 startValue = range.peggedValue(startValue); 700 startPegged = true; 701 } 702 double startPos = range.percent(startValue); 703 double endValue = rm.getEnd().getValue(); 704 boolean endPegged = false; 705 if (!range.contains(endValue)) { 706 endValue = range.peggedValue(endValue); 707 endPegged = true; 708 } 709 double endPos = range.percent(endValue); 710 markerData = new MarkerData(entry.getKey(), startPos, 711 startPegged, endPos, endPegged); 712 markerData.setLabelAnchor(rm.getLabel() != null 713 ? rm.getLabelAnchor() : null); 714 } else { 715 throw new RuntimeException("Unrecognised marker."); 716 } 717 result.add(markerData); 718 } 719 } 720 return result; 721 } 722 723 /** 724 * Receives a {@link ChartElementVisitor}. This method is part of a general 725 * mechanism for traversing the chart structure and performing operations 726 * on each element in the chart. You will not normally call this method 727 * directly. 728 * 729 * @param visitor the visitor ({@code null} not permitted). 730 * 731 * @since 1.2 732 */ 733 @Override 734 public void receive(ChartElementVisitor visitor) { 735 for (ValueMarker marker : this.valueMarkers.values()) { 736 marker.receive(visitor); 737 } 738 visitor.visit(this); 739 } 740 741 @Override 742 public boolean equals(Object obj) { 743 if (obj == this) { 744 return true; 745 } 746 if (!(obj instanceof AbstractValueAxis3D)) { 747 return false; 748 } 749 AbstractValueAxis3D that = (AbstractValueAxis3D) obj; 750 if (!this.range.equals(that.range)) { 751 return false; 752 } 753 if (this.autoAdjustRange != that.autoAdjustRange) { 754 return false; 755 } 756 if (this.lowerMargin != that.lowerMargin) { 757 return false; 758 } 759 if (this.upperMargin != that.upperMargin) { 760 return false; 761 } 762 if (!this.defaultAutoRange.equals(that.defaultAutoRange)) { 763 return false; 764 } 765 if (this.tickLabelOffset != that.tickLabelOffset) { 766 return false; 767 } 768 if (this.tickLabelFactor != that.tickLabelFactor) { 769 return false; 770 } 771 if (!this.tickLabelOrientation.equals(that.tickLabelOrientation)) { 772 return false; 773 } 774 if (this.tickMarkLength != that.tickMarkLength) { 775 return false; 776 } 777 if (!ObjectUtils.equalsPaint(this.tickMarkPaint, that.tickMarkPaint)) { 778 return false; 779 } 780 if (!this.tickMarkStroke.equals(that.tickMarkStroke)) { 781 return false; 782 } 783 return super.equals(obj); 784 } 785 786 /** 787 * Provides serialization support. 788 * 789 * @param stream the output stream. 790 * 791 * @throws IOException if there is an I/O error. 792 */ 793 private void writeObject(ObjectOutputStream stream) throws IOException { 794 stream.defaultWriteObject(); 795 SerialUtils.writePaint(this.tickMarkPaint, stream); 796 SerialUtils.writeStroke(this.tickMarkStroke, stream); 797 } 798 799 /** 800 * Provides serialization support. 801 * 802 * @param stream the input stream. 803 * 804 * @throws IOException if there is an I/O error. 805 * @throws ClassNotFoundException if there is a classpath problem. 806 */ 807 private void readObject(ObjectInputStream stream) 808 throws IOException, ClassNotFoundException { 809 stream.defaultReadObject(); 810 this.tickMarkPaint = SerialUtils.readPaint(stream); 811 this.tickMarkStroke = SerialUtils.readStroke(stream); 812 } 813 814}