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 &lt; max).
232     * @param max  the upper bound for the range (requires max &gt; 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}