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