001/* ===========================================================
002 * JFreeChart : a free chart library for the Java(tm) platform
003 * ===========================================================
004 *
005 * (C) Copyright 2000-present, by David Gilbert and Contributors.
006 *
007 * Project Info:  http://www.jfree.org/jfreechart/index.html
008 *
009 * This library is free software; you can redistribute it and/or modify it
010 * under the terms of the GNU Lesser General Public License as published by
011 * the Free Software Foundation; either version 2.1 of the License, or
012 * (at your option) any later version.
013 *
014 * This library is distributed in the hope that it will be useful, but
015 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
016 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
017 * License for more details.
018 *
019 * You should have received a copy of the GNU Lesser General Public
020 * License along with this library; if not, write to the Free Software
021 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301,
022 * USA.
023 *
024 * [Oracle and Java are registered trademarks of Oracle and/or its affiliates. 
025 * Other names may be trademarks of their respective owners.]
026 *
027 * ---------------
028 * SymbolAxis.java
029 * ---------------
030 * (C) Copyright 2002-present, by Anthony Boulestreau and Contributors.
031 *
032 * Original Author:  Anthony Boulestreau;
033 * Contributor(s):   David Gilbert;
034 *
035 */
036
037package org.jfree.chart.axis;
038
039import java.awt.BasicStroke;
040import java.awt.Color;
041import java.awt.Font;
042import java.awt.Graphics2D;
043import java.awt.Paint;
044import java.awt.Shape;
045import java.awt.Stroke;
046import java.awt.geom.Rectangle2D;
047import java.io.IOException;
048import java.io.ObjectInputStream;
049import java.io.ObjectOutputStream;
050import java.io.Serializable;
051import java.text.NumberFormat;
052import java.util.Arrays;
053import java.util.Iterator;
054import java.util.List;
055
056import org.jfree.chart.plot.Plot;
057import org.jfree.chart.plot.PlotRenderingInfo;
058import org.jfree.chart.plot.ValueAxisPlot;
059import org.jfree.chart.text.TextUtils;
060import org.jfree.chart.ui.RectangleEdge;
061import org.jfree.chart.ui.TextAnchor;
062import org.jfree.chart.util.PaintUtils;
063import org.jfree.chart.util.Args;
064import org.jfree.chart.util.SerialUtils;
065import org.jfree.data.Range;
066
067/**
068 * A standard linear value axis that replaces integer values with symbols.
069 */
070public class SymbolAxis extends NumberAxis implements Serializable {
071
072    /** For serialization. */
073    private static final long serialVersionUID = 7216330468770619716L;
074
075    /** The default grid band paint. */
076    public static final Paint DEFAULT_GRID_BAND_PAINT
077            = new Color(232, 234, 232, 128);
078
079    /**
080     * The default paint for alternate grid bands.
081     */
082    public static final Paint DEFAULT_GRID_BAND_ALTERNATE_PAINT
083            = new Color(0, 0, 0, 0);  // transparent
084
085    /** The list of symbols to display instead of the numeric values. */
086    private List symbols;
087
088    /** Flag that indicates whether or not grid bands are visible. */
089    private boolean gridBandsVisible;
090
091    /** The paint used to color the grid bands (if the bands are visible). */
092    private transient Paint gridBandPaint;
093
094    /**
095     * The paint used to fill the alternate grid bands.
096     */
097    private transient Paint gridBandAlternatePaint;
098
099    /**
100     * Constructs a symbol axis, using default attribute values where
101     * necessary.
102     *
103     * @param label  the axis label ({@code null} permitted).
104     * @param sv  the list of symbols to display instead of the numeric
105     *            values.
106     */
107    public SymbolAxis(String label, String[] sv) {
108        super(label);
109        this.symbols = Arrays.asList(sv);
110        this.gridBandsVisible = true;
111        this.gridBandPaint = DEFAULT_GRID_BAND_PAINT;
112        this.gridBandAlternatePaint = DEFAULT_GRID_BAND_ALTERNATE_PAINT;
113        setAutoTickUnitSelection(false, false);
114        setAutoRangeStickyZero(false);
115    }
116
117    /**
118     * Returns an array of the symbols for the axis.
119     *
120     * @return The symbols.
121     */
122    public String[] getSymbols() {
123        String[] result = new String[this.symbols.size()];
124        result = (String[]) this.symbols.toArray(result);
125        return result;
126    }
127
128    /**
129     * Returns the flag that controls whether or not grid bands are drawn for 
130     * the axis.  The default value is {@code true}. 
131     *
132     * @return A boolean.
133     *
134     * @see #setGridBandsVisible(boolean)
135     */
136    public boolean isGridBandsVisible() {
137        return this.gridBandsVisible;
138    }
139
140    /**
141     * Sets the flag that controls whether or not grid bands are drawn for this
142     * axis and notifies registered listeners that the axis has been modified.
143     * Each band is the area between two adjacent gridlines 
144     * running perpendicular to the axis.  When the bands are drawn they are 
145     * filled with the colors {@link #getGridBandPaint()} and 
146     * {@link #getGridBandAlternatePaint()} in an alternating sequence.
147     *
148     * @param flag  the new setting.
149     *
150     * @see #isGridBandsVisible()
151     */
152    public void setGridBandsVisible(boolean flag) {
153        this.gridBandsVisible = flag;
154        fireChangeEvent();
155    }
156
157    /**
158     * Returns the paint used to color grid bands (two colors are used
159     * alternately, the other is returned by 
160     * {@link #getGridBandAlternatePaint()}).  The default value is
161     * {@link #DEFAULT_GRID_BAND_PAINT}.
162     *
163     * @return The paint (never {@code null}).
164     *
165     * @see #setGridBandPaint(Paint)
166     * @see #isGridBandsVisible()
167     */
168    public Paint getGridBandPaint() {
169        return this.gridBandPaint;
170    }
171
172    /**
173     * Sets the grid band paint and notifies registered listeners that the
174     * axis has been changed.  See the {@link #setGridBandsVisible(boolean)}
175     * method for more information about grid bands.
176     *
177     * @param paint  the paint ({@code null} not permitted).
178     *
179     * @see #getGridBandPaint()
180     */
181    public void setGridBandPaint(Paint paint) {
182        Args.nullNotPermitted(paint, "paint");
183        this.gridBandPaint = paint;
184        fireChangeEvent();
185    }
186
187    /**
188     * Returns the second paint used to color grid bands (two colors are used
189     * alternately, the other is returned by {@link #getGridBandPaint()}).  
190     * The default value is {@link #DEFAULT_GRID_BAND_ALTERNATE_PAINT} 
191     * (transparent).
192     *
193     * @return The paint (never {@code null}).
194     *
195     * @see #setGridBandAlternatePaint(Paint)
196     */
197    public Paint getGridBandAlternatePaint() {
198        return this.gridBandAlternatePaint;
199    }
200
201    /**
202     * Sets the grid band paint and notifies registered listeners that the
203     * axis has been changed.  See the {@link #setGridBandsVisible(boolean)}
204     * method for more information about grid bands.
205     *
206     * @param paint  the paint ({@code null} not permitted).
207     *
208     * @see #getGridBandAlternatePaint()
209     * @see #setGridBandPaint(Paint)
210     */
211    public void setGridBandAlternatePaint(Paint paint) {
212        Args.nullNotPermitted(paint, "paint");
213        this.gridBandAlternatePaint = paint;
214        fireChangeEvent();
215    }
216
217    /**
218     * This operation is not supported by this axis.
219     *
220     * @param g2  the graphics device.
221     * @param dataArea  the area in which the plot and axes should be drawn.
222     * @param edge  the edge along which the axis is drawn.
223     */
224    @Override
225    protected void selectAutoTickUnit(Graphics2D g2, Rectangle2D dataArea,
226            RectangleEdge edge) {
227        throw new UnsupportedOperationException();
228    }
229
230    /**
231     * Draws the axis on a Java 2D graphics device (such as the screen or a
232     * printer).
233     *
234     * @param g2  the graphics device ({@code null} not permitted).
235     * @param cursor  the cursor location.
236     * @param plotArea  the area within which the plot and axes should be drawn
237     *                  ({@code null} not permitted).
238     * @param dataArea  the area within which the data should be drawn
239     *                  ({@code null} not permitted).
240     * @param edge  the axis location ({@code null} not permitted).
241     * @param plotState  collects information about the plot
242     *                   ({@code null} permitted).
243     *
244     * @return The axis state (never {@code null}).
245     */
246    @Override
247    public AxisState draw(Graphics2D g2, double cursor, Rectangle2D plotArea,
248            Rectangle2D dataArea, RectangleEdge edge, 
249            PlotRenderingInfo plotState) {
250
251        AxisState info = new AxisState(cursor);
252        if (isVisible()) {
253            info = super.draw(g2, cursor, plotArea, dataArea, edge, plotState);
254        }
255        if (this.gridBandsVisible) {
256            drawGridBands(g2, plotArea, dataArea, edge, info.getTicks());
257        }
258        return info;
259
260    }
261
262    /**
263     * Draws the grid bands (alternate bands are colored using
264     * {@link #getGridBandPaint()} and {@link #getGridBandAlternatePaint()}.
265     *
266     * @param g2  the graphics target ({@code null} not permitted).
267     * @param plotArea  the area within which the plot is drawn 
268     *     ({@code null} not permitted).
269     * @param dataArea  the data area to which the axes are aligned 
270     *     ({@code null} not permitted).
271     * @param edge  the edge to which the axis is aligned ({@code null} not
272     *     permitted).
273     * @param ticks  the ticks ({@code null} not permitted).
274     */
275    protected void drawGridBands(Graphics2D g2, Rectangle2D plotArea,
276            Rectangle2D dataArea, RectangleEdge edge, List ticks) {
277        Shape savedClip = g2.getClip();
278        g2.clip(dataArea);
279        if (RectangleEdge.isTopOrBottom(edge)) {
280            drawGridBandsHorizontal(g2, plotArea, dataArea, true, ticks);
281        } else if (RectangleEdge.isLeftOrRight(edge)) {
282            drawGridBandsVertical(g2, plotArea, dataArea, true, ticks);
283        }
284        g2.setClip(savedClip);
285    }
286
287    /**
288     * Draws the grid bands for the axis when it is at the top or bottom of
289     * the plot.
290     *
291     * @param g2  the graphics target ({@code null} not permitted).
292     * @param plotArea  the area within which the plot is drawn (not used here).
293     * @param dataArea  the area for the data (to which the axes are aligned,
294     *         {@code null} not permitted).
295     * @param firstGridBandIsDark  True: the first grid band takes the
296     *                             color of {@code gridBandPaint}.
297     *                             False: the second grid band takes the
298     *                             color of {@code gridBandPaint}.
299     * @param ticks  a list of ticks ({@code null} not permitted).
300     */
301    protected void drawGridBandsHorizontal(Graphics2D g2,
302            Rectangle2D plotArea, Rectangle2D dataArea, 
303            boolean firstGridBandIsDark, List ticks) {
304
305        boolean currentGridBandIsDark = firstGridBandIsDark;
306        double yy = dataArea.getY();
307        double xx1, xx2;
308
309        //gets the outline stroke width of the plot
310        double outlineStrokeWidth = 1.0;
311        Stroke outlineStroke = getPlot().getOutlineStroke();
312        if (outlineStroke != null && outlineStroke instanceof BasicStroke) {
313            outlineStrokeWidth = ((BasicStroke) outlineStroke).getLineWidth();
314        }
315
316        Iterator iterator = ticks.iterator();
317        ValueTick tick;
318        Rectangle2D band;
319        while (iterator.hasNext()) {
320            tick = (ValueTick) iterator.next();
321            xx1 = valueToJava2D(tick.getValue() - 0.5d, dataArea,
322                    RectangleEdge.BOTTOM);
323            xx2 = valueToJava2D(tick.getValue() + 0.5d, dataArea,
324                    RectangleEdge.BOTTOM);
325            if (currentGridBandIsDark) {
326                g2.setPaint(this.gridBandPaint);
327            } else {
328                g2.setPaint(this.gridBandAlternatePaint);
329            }
330            band = new Rectangle2D.Double(Math.min(xx1, xx2), 
331                    yy + outlineStrokeWidth, Math.abs(xx2 - xx1), 
332                    dataArea.getMaxY() - yy - outlineStrokeWidth);
333            g2.fill(band);
334            currentGridBandIsDark = !currentGridBandIsDark;
335        }
336    }
337
338    /**
339     * Draws the grid bands for an axis that is aligned to the left or
340     * right of the data area (that is, a vertical axis).
341     *
342     * @param g2  the graphics target ({@code null} not permitted).
343     * @param plotArea  the area within which the plot is drawn (not used here).
344     * @param dataArea  the area for the data (to which the axes are aligned,
345     *         {@code null} not permitted).
346     * @param firstGridBandIsDark  True: the first grid band takes the
347     *                             color of {@code gridBandPaint}.
348     *                             False: the second grid band takes the
349     *                             color of {@code gridBandPaint}.
350     * @param ticks  a list of ticks ({@code null} not permitted).
351     */
352    protected void drawGridBandsVertical(Graphics2D g2, Rectangle2D plotArea,
353            Rectangle2D dataArea, boolean firstGridBandIsDark, 
354            List ticks) {
355
356        boolean currentGridBandIsDark = firstGridBandIsDark;
357        double xx = dataArea.getX();
358        double yy1, yy2;
359
360        //gets the outline stroke width of the plot
361        double outlineStrokeWidth = 1.0;
362        Stroke outlineStroke = getPlot().getOutlineStroke();
363        if (outlineStroke != null && outlineStroke instanceof BasicStroke) {
364            outlineStrokeWidth = ((BasicStroke) outlineStroke).getLineWidth();
365        }
366
367        Iterator iterator = ticks.iterator();
368        ValueTick tick;
369        Rectangle2D band;
370        while (iterator.hasNext()) {
371            tick = (ValueTick) iterator.next();
372            yy1 = valueToJava2D(tick.getValue() + 0.5d, dataArea,
373                    RectangleEdge.LEFT);
374            yy2 = valueToJava2D(tick.getValue() - 0.5d, dataArea,
375                    RectangleEdge.LEFT);
376            if (currentGridBandIsDark) {
377                g2.setPaint(this.gridBandPaint);
378            } else {
379                g2.setPaint(this.gridBandAlternatePaint);
380            }
381            band = new Rectangle2D.Double(xx + outlineStrokeWidth, 
382                    Math.min(yy1, yy2), dataArea.getMaxX() - xx 
383                    - outlineStrokeWidth, Math.abs(yy2 - yy1));
384            g2.fill(band);
385            currentGridBandIsDark = !currentGridBandIsDark;
386        }
387    }
388
389    /**
390     * Rescales the axis to ensure that all data is visible.
391     */
392    @Override
393    protected void autoAdjustRange() {
394        Plot plot = getPlot();
395        if (plot == null) {
396            return;  // no plot, no data
397        }
398
399        if (plot instanceof ValueAxisPlot) {
400
401            // ensure that all the symbols are displayed
402            double upper = this.symbols.size() - 1;
403            double lower = 0;
404            double range = upper - lower;
405
406            // ensure the autorange is at least <minRange> in size...
407            double minRange = getAutoRangeMinimumSize();
408            if (range < minRange) {
409                upper = (upper + lower + minRange) / 2;
410                lower = (upper + lower - minRange) / 2;
411            }
412
413            // this ensure that the grid bands will be displayed correctly.
414            double upperMargin = 0.5;
415            double lowerMargin = 0.5;
416
417            if (getAutoRangeIncludesZero()) {
418                if (getAutoRangeStickyZero()) {
419                    if (upper <= 0.0) {
420                        upper = 0.0;
421                    } else {
422                        upper = upper + upperMargin;
423                    }
424                    if (lower >= 0.0) {
425                        lower = 0.0;
426                    } else {
427                        lower = lower - lowerMargin;
428                    }
429                } else {
430                    upper = Math.max(0.0, upper + upperMargin);
431                    lower = Math.min(0.0, lower - lowerMargin);
432                }
433            } else {
434                if (getAutoRangeStickyZero()) {
435                    if (upper <= 0.0) {
436                        upper = Math.min(0.0, upper + upperMargin);
437                    } else {
438                        upper = upper + upperMargin * range;
439                    }
440                    if (lower >= 0.0) {
441                        lower = Math.max(0.0, lower - lowerMargin);
442                    } else {
443                        lower = lower - lowerMargin;
444                    }
445                } else {
446                    upper = upper + upperMargin;
447                    lower = lower - lowerMargin;
448                }
449            }
450            setRange(new Range(lower, upper), false, false);
451        }
452    }
453
454    /**
455     * Calculates the positions of the tick labels for the axis, storing the
456     * results in the tick label list (ready for drawing).
457     *
458     * @param g2  the graphics device.
459     * @param state  the axis state.
460     * @param dataArea  the area in which the data should be drawn.
461     * @param edge  the location of the axis.
462     *
463     * @return A list of ticks.
464     */
465    @Override
466    public List refreshTicks(Graphics2D g2, AxisState state,
467            Rectangle2D dataArea, RectangleEdge edge) {
468        List ticks = null;
469        if (RectangleEdge.isTopOrBottom(edge)) {
470            ticks = refreshTicksHorizontal(g2, dataArea, edge);
471        } else if (RectangleEdge.isLeftOrRight(edge)) {
472            ticks = refreshTicksVertical(g2, dataArea, edge);
473        }
474        return ticks;
475    }
476
477    /**
478     * Calculates the positions of the tick labels for the axis, storing the
479     * results in the tick label list (ready for drawing).
480     *
481     * @param g2  the graphics device.
482     * @param dataArea  the area in which the data should be drawn.
483     * @param edge  the location of the axis.
484     *
485     * @return The ticks.
486     */
487    @Override
488    protected List refreshTicksHorizontal(Graphics2D g2, Rectangle2D dataArea,
489            RectangleEdge edge) {
490
491        List ticks = new java.util.ArrayList();
492
493        Font tickLabelFont = getTickLabelFont();
494        g2.setFont(tickLabelFont);
495
496        double size = getTickUnit().getSize();
497        int count = calculateVisibleTickCount();
498        double lowestTickValue = calculateLowestVisibleTickValue();
499
500        double previousDrawnTickLabelPos = 0.0;
501        double previousDrawnTickLabelLength = 0.0;
502
503        if (count <= ValueAxis.MAXIMUM_TICK_COUNT) {
504            for (int i = 0; i < count; i++) {
505                double currentTickValue = lowestTickValue + (i * size);
506                double xx = valueToJava2D(currentTickValue, dataArea, edge);
507                String tickLabel;
508                NumberFormat formatter = getNumberFormatOverride();
509                if (formatter != null) {
510                    tickLabel = formatter.format(currentTickValue);
511                }
512                else {
513                    tickLabel = valueToString(currentTickValue);
514                }
515
516                // avoid to draw overlapping tick labels
517                Rectangle2D bounds = TextUtils.getTextBounds(tickLabel, g2,
518                        g2.getFontMetrics());
519                double tickLabelLength = isVerticalTickLabels()
520                        ? bounds.getHeight() : bounds.getWidth();
521                boolean tickLabelsOverlapping = false;
522                if (i > 0) {
523                    double avgTickLabelLength = (previousDrawnTickLabelLength
524                            + tickLabelLength) / 2.0;
525                    if (Math.abs(xx - previousDrawnTickLabelPos)
526                            < avgTickLabelLength) {
527                        tickLabelsOverlapping = true;
528                    }
529                }
530                if (tickLabelsOverlapping) {
531                    tickLabel = ""; // don't draw this tick label
532                }
533                else {
534                    // remember these values for next comparison
535                    previousDrawnTickLabelPos = xx;
536                    previousDrawnTickLabelLength = tickLabelLength;
537                }
538
539                TextAnchor anchor;
540                TextAnchor rotationAnchor;
541                double angle = 0.0;
542                if (isVerticalTickLabels()) {
543                    anchor = TextAnchor.CENTER_RIGHT;
544                    rotationAnchor = TextAnchor.CENTER_RIGHT;
545                    if (edge == RectangleEdge.TOP) {
546                        angle = Math.PI / 2.0;
547                    }
548                    else {
549                        angle = -Math.PI / 2.0;
550                    }
551                }
552                else {
553                    if (edge == RectangleEdge.TOP) {
554                        anchor = TextAnchor.BOTTOM_CENTER;
555                        rotationAnchor = TextAnchor.BOTTOM_CENTER;
556                    }
557                    else {
558                        anchor = TextAnchor.TOP_CENTER;
559                        rotationAnchor = TextAnchor.TOP_CENTER;
560                    }
561                }
562                Tick tick = new NumberTick(currentTickValue,
563                        tickLabel, anchor, rotationAnchor, angle);
564                ticks.add(tick);
565            }
566        }
567        return ticks;
568
569    }
570
571    /**
572     * Calculates the positions of the tick labels for the axis, storing the
573     * results in the tick label list (ready for drawing).
574     *
575     * @param g2  the graphics device.
576     * @param dataArea  the area in which the plot should be drawn.
577     * @param edge  the location of the axis.
578     *
579     * @return The ticks.
580     */
581    @Override
582    protected List refreshTicksVertical(Graphics2D g2, Rectangle2D dataArea,
583            RectangleEdge edge) {
584
585        List ticks = new java.util.ArrayList();
586
587        Font tickLabelFont = getTickLabelFont();
588        g2.setFont(tickLabelFont);
589
590        double size = getTickUnit().getSize();
591        int count = calculateVisibleTickCount();
592        double lowestTickValue = calculateLowestVisibleTickValue();
593
594        double previousDrawnTickLabelPos = 0.0;
595        double previousDrawnTickLabelLength = 0.0;
596
597        if (count <= ValueAxis.MAXIMUM_TICK_COUNT) {
598            for (int i = 0; i < count; i++) {
599                double currentTickValue = lowestTickValue + (i * size);
600                double yy = valueToJava2D(currentTickValue, dataArea, edge);
601                String tickLabel;
602                NumberFormat formatter = getNumberFormatOverride();
603                if (formatter != null) {
604                    tickLabel = formatter.format(currentTickValue);
605                }
606                else {
607                    tickLabel = valueToString(currentTickValue);
608                }
609
610                // avoid to draw overlapping tick labels
611                Rectangle2D bounds = TextUtils.getTextBounds(tickLabel, g2,
612                        g2.getFontMetrics());
613                double tickLabelLength = isVerticalTickLabels()
614                    ? bounds.getWidth() : bounds.getHeight();
615                boolean tickLabelsOverlapping = false;
616                if (i > 0) {
617                    double avgTickLabelLength = (previousDrawnTickLabelLength
618                            + tickLabelLength) / 2.0;
619                    if (Math.abs(yy - previousDrawnTickLabelPos)
620                            < avgTickLabelLength) {
621                        tickLabelsOverlapping = true;
622                    }
623                }
624                if (tickLabelsOverlapping) {
625                    tickLabel = ""; // don't draw this tick label
626                }
627                else {
628                    // remember these values for next comparison
629                    previousDrawnTickLabelPos = yy;
630                    previousDrawnTickLabelLength = tickLabelLength;
631                }
632
633                TextAnchor anchor;
634                TextAnchor rotationAnchor;
635                double angle = 0.0;
636                if (isVerticalTickLabels()) {
637                    anchor = TextAnchor.BOTTOM_CENTER;
638                    rotationAnchor = TextAnchor.BOTTOM_CENTER;
639                    if (edge == RectangleEdge.LEFT) {
640                        angle = -Math.PI / 2.0;
641                    }
642                    else {
643                        angle = Math.PI / 2.0;
644                    }
645                }
646                else {
647                    if (edge == RectangleEdge.LEFT) {
648                        anchor = TextAnchor.CENTER_RIGHT;
649                        rotationAnchor = TextAnchor.CENTER_RIGHT;
650                    }
651                    else {
652                        anchor = TextAnchor.CENTER_LEFT;
653                        rotationAnchor = TextAnchor.CENTER_LEFT;
654                    }
655                }
656                Tick tick = new NumberTick(currentTickValue, tickLabel, anchor, 
657                        rotationAnchor, angle);
658                ticks.add(tick);
659            }
660        }
661        return ticks;
662
663    }
664
665    /**
666     * Converts a value to a string, using the list of symbols.
667     *
668     * @param value  value to convert.
669     *
670     * @return The symbol.
671     */
672    public String valueToString(double value) {
673        String strToReturn;
674        try {
675            strToReturn = (String) this.symbols.get((int) value);
676        }
677        catch (IndexOutOfBoundsException  ex) {
678            strToReturn = "";
679        }
680        return strToReturn;
681    }
682
683    /**
684     * Tests this axis for equality with an arbitrary object.
685     *
686     * @param obj  the object ({@code null} permitted).
687     *
688     * @return A boolean.
689     */
690    @Override
691    public boolean equals(Object obj) {
692        if (obj == this) {
693            return true;
694        }
695        if (!(obj instanceof SymbolAxis)) {
696            return false;
697        }
698        SymbolAxis that = (SymbolAxis) obj;
699        if (!this.symbols.equals(that.symbols)) {
700            return false;
701        }
702        if (this.gridBandsVisible != that.gridBandsVisible) {
703            return false;
704        }
705        if (!PaintUtils.equal(this.gridBandPaint, that.gridBandPaint)) {
706            return false;
707        }
708        if (!PaintUtils.equal(this.gridBandAlternatePaint,
709                that.gridBandAlternatePaint)) {
710            return false;
711        }
712        return super.equals(obj);
713    }
714
715    /**
716     * Provides serialization support.
717     *
718     * @param stream  the output stream.
719     *
720     * @throws IOException  if there is an I/O error.
721     */
722    private void writeObject(ObjectOutputStream stream) throws IOException {
723        stream.defaultWriteObject();
724        SerialUtils.writePaint(this.gridBandPaint, stream);
725        SerialUtils.writePaint(this.gridBandAlternatePaint, stream);
726    }
727
728    /**
729     * Provides serialization support.
730     *
731     * @param stream  the input stream.
732     *
733     * @throws IOException  if there is an I/O error.
734     * @throws ClassNotFoundException  if there is a classpath problem.
735     */
736    private void readObject(ObjectInputStream stream)
737        throws IOException, ClassNotFoundException {
738        stream.defaultReadObject();
739        this.gridBandPaint = SerialUtils.readPaint(stream);
740        this.gridBandAlternatePaint = SerialUtils.readPaint(stream);
741    }
742
743}