    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}