    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 @SuppressWarnings("unchecked")
    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}