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