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.Graphics2D;
038import java.awt.Paint;
039import java.awt.Shape;
040import java.awt.Stroke;
041import java.awt.font.LineMetrics;
042import java.awt.geom.Line2D;
043import java.awt.geom.Point2D;
044import java.io.IOException;
045import java.io.ObjectInputStream;
046import java.io.ObjectOutputStream;
047import java.io.Serializable;
048import java.util.ArrayList;
049import java.util.LinkedHashMap;
050import java.util.List;
051import java.util.HashMap;
052import java.util.Map;
053
054import org.jfree.chart3d.graphics3d.internal.Utils2D;
055import org.jfree.chart3d.Chart3DHints;
056import org.jfree.chart3d.ChartElementVisitor;
057import org.jfree.chart3d.data.Range;
058import org.jfree.chart3d.data.category.CategoryDataset3D;
059import org.jfree.chart3d.graphics2d.TextAnchor;
060import org.jfree.chart3d.graphics3d.RenderedElement;
061import org.jfree.chart3d.graphics3d.RenderingInfo;
062import org.jfree.chart3d.interaction.InteractiveElementType;
063import org.jfree.chart3d.internal.Args;
064import org.jfree.chart3d.internal.ObjectUtils;
065import org.jfree.chart3d.internal.SerialUtils;
066import org.jfree.chart3d.internal.TextUtils;
067import org.jfree.chart3d.label.CategoryLabelGenerator;
068import org.jfree.chart3d.label.StandardCategoryLabelGenerator;
069import org.jfree.chart3d.marker.CategoryMarker;
070import org.jfree.chart3d.marker.CategoryMarkerType;
071import org.jfree.chart3d.marker.Marker;
072import org.jfree.chart3d.marker.MarkerData;
073import org.jfree.chart3d.plot.CategoryPlot3D;
074import org.jfree.chart3d.renderer.category.AreaRenderer3D;
075
076/**
077 * An axis that displays categories.
078 * <br><br>
079 * NOTE: This class is serializable, but the serialization format is subject 
080 * to change in future releases and should not be relied upon for persisting 
081 * instances of this class. 
082 */
083@SuppressWarnings("serial")
084public class StandardCategoryAxis3D extends AbstractAxis3D 
085        implements CategoryAxis3D, Serializable {
086    
087    /** The categories. */
088    private List<Comparable<?>> categories;
089  
090    /** 
091     * The axis range (never {@code null}). 
092     */
093    private Range range;
094    
095    private boolean inverted;
096
097    /** The percentage margin to leave at the lower end of the axis. */
098    private double lowerMargin;
099    
100    /** The percentage margin to leave at the upper end of the axis. */
101    private double upperMargin;
102
103    /** 
104     * Hide half of the first category?  This brings the category label 
105     * closer to the beginning of the axis.  It is useful if the renderer 
106     * doesn't make full use of the category space for the first item.
107     */
108    private boolean firstCategoryHalfWidth = false;
109    
110    /** 
111     * Hide half of the last category?  This brings the category label 
112     * closer to the end of the axis.  It is useful if the renderer 
113     * doesn't make full use of the category space for the last item.
114     */
115    private boolean lastCategoryHalfWidth = false;
116        
117    /** 
118     * The tick mark length (in Java2D units).  When this is 0.0, no tick
119     * marks will be drawn.
120     */
121    private double tickMarkLength;
122    
123    /** The tick mark stroke (never {@code null}). */
124    private transient Stroke tickMarkStroke;
125    
126    /** The tick mark paint (never {@code null}). */
127    private transient Paint tickMarkPaint;
128    
129    /** The tick label generator. */
130    private CategoryLabelGenerator tickLabelGenerator;
131    
132    /** 
133     * The tick label offset (in Java2D units).  This is the gap between the
134     * tick marks and their associated labels.
135     */
136    private double tickLabelOffset;
137    
138    /** The orientation for the tick labels. */
139    private LabelOrientation tickLabelOrientation;
140
141    /** 
142     * The maximum number of offset levels to use for tick labels on the axis. 
143     */
144    private int maxTickLabelLevels = 3;
145    
146    /**
147     * The tick label factor (used as a multiplier for the tick label width
148     * when checking for overlapping labels).  
149     */
150    private double tickLabelFactor = 1.2;
151    
152    /** 
153     * The markers for the axis (this may be empty, but not {@code null}). 
154     */
155    private Map<String, CategoryMarker> markers;
156    
157    /** A flag to indicate that this axis has been configured as a row axis. */
158    private boolean isRowAxis;
159    
160    /** 
161     * A flag to indicate that this axis has been configured as a column 
162     * axis. 
163     */
164    private boolean isColumnAxis;
165    
166    /**
167     * Default constructor.
168     */
169    public StandardCategoryAxis3D() {
170        this(null);
171    }
172 
173    /**
174     * Creates a new axis with the specified label.
175     * 
176     * @param label  the axis label ({@code null} permitted). 
177     */
178    public StandardCategoryAxis3D(String label) {
179        super(label);
180        this.categories = new ArrayList<>();
181        this.range = new Range(0.0, 1.0);
182        this.lowerMargin = 0.05;
183        this.upperMargin = 0.05;
184        this.firstCategoryHalfWidth = false;
185        this.lastCategoryHalfWidth = false;
186        this.tickMarkLength = 3.0;
187        this.tickMarkPaint = Color.GRAY;
188        this.tickMarkStroke = new BasicStroke(0.5f);
189        this.tickLabelGenerator = new StandardCategoryLabelGenerator();
190        this.tickLabelOffset = 5.0;
191        this.tickLabelOrientation = LabelOrientation.PARALLEL;
192        this.tickLabelFactor = 1.4;
193        this.maxTickLabelLevels = 3;
194        this.markers = new LinkedHashMap<>();
195        this.isRowAxis = false;
196        this.isColumnAxis = false;
197    }
198
199    /**
200     * Returns {@code true} if this axis has been configured as a 
201     * row axis for the plot that it belongs to, and {@code false} 
202     * otherwise.
203     * 
204     * @return A boolean.
205     * 
206     * @since 1.3
207     */
208    @Override
209    public boolean isRowAxis() {
210        return isRowAxis;
211    }
212
213    /**
214     * Returns {@code true} if this axis has been configured as a
215     * column axis for the plot that it belongs to, and {@code false}
216     * otherwise.
217     * 
218     * @return A boolean.
219     * 
220     * @since 1.3
221     */
222    @Override
223    public boolean isColumnAxis() {
224        return isColumnAxis;
225    }
226
227    /**
228     * Returns the range for the axis.  By convention, the category axes have
229     * a range from 0.0 to 1.0.
230     * 
231     * @return The range. 
232     */
233    @Override
234    public Range getRange() {
235        return this.range;
236    }
237
238    /**
239     * Sets the range for the axis and sends an {@link Axis3DChangeEvent} to
240     * all registered listeners.
241     * 
242     * @param lowerBound  the lower bound.
243     * @param upperBound  the upper bound.
244     */
245    @Override
246    public void setRange(double lowerBound, double upperBound) {
247        setRange(new Range(lowerBound, upperBound));
248    }
249
250    /**
251     * Sets the range for the axis and sends an {@link Axis3DChangeEvent} to
252     * all registered listeners. Note that changing the range for the 
253     * category axis will have no visible effect.
254     * 
255     * @param range  the range ({@code null} not permitted). 
256     */
257    @Override
258    public void setRange(Range range) {
259        Args.nullNotPermitted(range, "range");
260        this.range = range;
261        fireChangeEvent(true);
262    }
263    
264    /**
265     * Returns the margin to leave at the lower end of the axis, as a 
266     * percentage of the axis length.  The default is {@code 0.05} (five 
267     * percent).
268     * 
269     * @return The lower margin.
270     */
271    public double getLowerMargin() {
272        return this.lowerMargin;
273    }
274    
275    /**
276     * Sets the margin to leave at the lower end of the axis and sends an
277     * {@link Axis3DChangeEvent} to all registered listeners.
278     * 
279     * @param margin  the margin. 
280     */
281    public void setLowerMargin(double margin) {
282        this.lowerMargin = margin;
283        fireChangeEvent(true);
284    }
285    
286    /**
287     * Returns the margin to leave at the upper end of the axis, as a 
288     * percentage of the axis length.  The default is {@code 0.05} (five 
289     * percent).
290     * 
291     * @return The lower margin.
292     */
293    public double getUpperMargin() {
294        return this.upperMargin;
295    }
296    
297    /**
298     * Sets the margin to leave at the upper end of the axis and sends an
299     * {@link Axis3DChangeEvent} to all registered listeners.
300     * 
301     * @param margin  the margin. 
302     */
303    public void setUpperMargin(double margin) {
304        this.upperMargin = margin;
305        fireChangeEvent(true);
306    }
307    
308    /**
309     * Returns {@code true} if the first category on the axis should
310     * occupy half the normal width, and {@code false} otherwise.
311     * 
312     * @return A boolean.
313     * 
314     * @see #setFirstCategoryHalfWidth(boolean) 
315     */
316    public boolean isFirstCategoryHalfWidth() {
317        return this.firstCategoryHalfWidth;
318    }
319    
320    /**
321     * Sets the flag that controls whether the first category on the axis 
322     * occupies a full or half width, and sends an {@link Axis3DChangeEvent}
323     * to all registered listeners.  There are some renderers where the 
324     * charts look better when half-widths are used (for example,
325     * {@link AreaRenderer3D}).
326     * 
327     * @param half  half width?
328     * 
329     * @see #setLastCategoryHalfWidth(boolean) 
330     */
331    public void setFirstCategoryHalfWidth(boolean half) {
332        this.firstCategoryHalfWidth = half;
333        fireChangeEvent(true);
334    }
335    
336    /**
337     * Returns {@code true} if the last category on the axis should
338     * occupy half the normal width, and {@code false} otherwise.
339     * 
340     * @return A boolean.
341     * 
342     * @see #setLastCategoryHalfWidth(boolean) 
343     */
344    public boolean isLastCategoryHalfWidth() {
345        return this.lastCategoryHalfWidth;
346    }
347    
348    /**
349     * Sets the flag that controls whether the last category on the axis 
350     * occupies a full or half width, and sends an {@link Axis3DChangeEvent}
351     * to all registered listeners.  There are some renderers where the 
352     * charts look better when half-widths are used (for example,
353     * {@link AreaRenderer3D}).
354     * 
355     * @param half  half width?
356     * 
357     * @see #setFirstCategoryHalfWidth(boolean) 
358     */
359    public void setLastCategoryHalfWidth(boolean half) {
360        this.lastCategoryHalfWidth = half;
361        fireChangeEvent(true);
362    }
363
364    /**
365     * Returns the tick mark length (in Java2D units).  The default value
366     * is {@code 3.0}.
367     * 
368     * @return The tick mark length. 
369     */
370    public double getTickMarkLength() {
371        return this.tickMarkLength;
372    }
373    
374    /**
375     * Sets the tick mark length (in Java2D units) and sends an 
376     * {@link Axis3DChangeEvent} to all registered listeners.  You can set
377     * the length to {@code 0.0} if you don't want any tick marks on the
378     * axis.
379     * 
380     * @param length  the length (in Java2D units).
381     */
382    public void setTickMarkLength(double length) {
383        this.tickMarkLength = length;
384        fireChangeEvent(false);
385    }
386    
387    /**
388     * Returns the paint used to draw the tick marks, if they are visible.  
389     * The default value is {@code Color.GRAY}.
390     * 
391     * @return The paint used to draw the tick marks (never {@code null}). 
392     */
393    public Paint getTickMarkPaint() {
394        return this.tickMarkPaint;
395    }
396
397    /**
398     * Sets the paint used to draw the tick marks and sends an 
399     * {@link Axis3DChangeEvent} to all registered listeners.
400     * 
401     * @param paint  the paint ({@code null} not permitted). 
402     */
403    public void setTickMarkPaint(Paint paint) {
404        Args.nullNotPermitted(paint, "paint");
405        this.tickMarkPaint = paint;
406        fireChangeEvent(false);
407    }
408    
409    /**
410     * Returns the stroke used to draw the tick marks, if they are visible.  
411     * The default value is {@code new BasicStroke(0.5f)}.
412     * 
413     * @return The stroke used to draw the tick marks (never {@code null}). 
414     */
415    public Stroke getTickMarkStroke() {
416        return this.tickMarkStroke;
417    }
418    
419    /**
420     * Sets the stroke used to draw the tick marks and sends an 
421     * {@link Axis3DChangeEvent} to all registered listeners.
422     * 
423     * @param stroke  the stroke ({@code null} not permitted). 
424     */
425    public void setTickMarkStroke(Stroke stroke) {
426        Args.nullNotPermitted(stroke, "stroke");
427        this.tickMarkStroke = stroke;
428        fireChangeEvent(false);
429    }
430    
431    /**
432     * Returns the tick label generator for the axis.  This is an object that
433     * is responsible for creating the category labels on the axis.  You can
434     * plug in your own instance to take full control over the generation
435     * of category labels.
436     * 
437     * @return The tick label generator for the axis (never {@code null}). 
438     * 
439     * @since 1.2
440     */
441    public CategoryLabelGenerator getTickLabelGenerator() {
442        return this.tickLabelGenerator;
443    }
444    
445    /**
446     * Sets the tick label generator for the axis and sends a change event to 
447     * all registered listeners.
448     * 
449     * @param generator  the generator ({@code null} not permitted).
450     * 
451     * @since 1.2
452     */
453    public void setTickLabelGenerator(CategoryLabelGenerator generator) {
454        Args.nullNotPermitted(generator, "generator");
455        this.tickLabelGenerator = generator;
456        fireChangeEvent(false);
457    }
458    
459    /**
460     * Returns the offset between the tick marks and the tick labels.  The
461     * default value is {@code 5.0}.
462     * 
463     * @return The offset between the tick marks and the tick labels (in Java2D
464     *     units).
465     */
466    public double getTickLabelOffset() {
467        return this.tickLabelOffset;
468    }
469    
470    /**
471     * Sets the offset between the tick marks and the tick labels and sends
472     * a {@link Axis3DChangeEvent} to all registered listeners.
473     * 
474     * @param offset  the offset.
475     */
476    public void setTickLabelOffset(double offset) {
477        this.tickLabelOffset = offset;
478        fireChangeEvent(false);
479    }
480    
481    /**
482     * Returns the orientation for the tick labels.  The default value is
483     * {@link LabelOrientation#PARALLEL}.
484     * 
485     * @return The orientation for the tick labels (never {@code null}).
486     * 
487     * @since 1.2
488     */
489    public LabelOrientation getTickLabelOrientation() {
490        return this.tickLabelOrientation;
491    }
492    
493    /**
494     * Sets the orientation for the tick labels and sends a change event to
495     * all registered listeners.
496     * 
497     * @param orientation  the orientation ({@code null} not permitted).
498     * 
499     * @since 1.2
500     */
501    public void setTickLabelOrientation(LabelOrientation orientation) {
502        Args.nullNotPermitted(orientation, "orientation");
503        this.tickLabelOrientation = orientation;
504        fireChangeEvent(false);
505    }
506    
507    /**
508     * Returns the maximum number of offset levels for the category labels on 
509     * the axis.  The default value is 3.
510     * 
511     * @return The maximum number of offset levels.
512     * 
513     * @since 1.2
514     */
515    public int getMaxTickLabelLevels() {
516        return this.maxTickLabelLevels;
517    }
518    
519    /**
520     * Sets the maximum number of offset levels for the category labels on the
521     * axis and sends a change event to all registered listeners.
522     * 
523     * @param levels  the maximum number of levels.
524     * 
525     * @since 1.2
526     */
527    public void setMaxTickLabelLevels(int levels) {
528        this.maxTickLabelLevels = levels;
529        fireChangeEvent(false);
530    }
531 
532    /**
533     * Returns the tick label factor.  The default value is {@code 1.4}.
534     * 
535     * @return The tick label factor.
536     * 
537     * @since 1.2
538     */
539    public double getTickLabelFactor() {
540        return this.tickLabelFactor;
541    }
542    
543    /**
544     * Sets the tick label factor and sends a change event to all registered 
545     * listeners.  
546     * 
547     * @param factor  the new factor (should be at least 1.0).
548     * 
549     * @since 1.2
550     */
551    public void setTickLabelFactor(double factor) {
552        this.tickLabelFactor = factor;
553        fireChangeEvent(false);
554    }
555    
556    /**
557     * Returns the marker with the specified key, if there is one.
558     * 
559     * @param key  the key ({@code null} not permitted).
560     * 
561     * @return The marker (possibly {@code null}). 
562     * 
563     * @since 1.2
564     */
565    @Override
566    public CategoryMarker getMarker(String key) {
567        return this.markers.get(key);
568    }
569
570    /**
571     * Sets the marker for the specified key and sends a change event to 
572     * all registered listeners.  If there is an existing marker it is replaced
573     * (and the axis will no longer listen for change events on the previous 
574     * marker).
575     * 
576     * @param key  the key that identifies the marker ({@code null} not 
577     *         permitted).
578     * @param marker  the marker ({@code null} permitted).
579     * 
580     * @since 1.2
581     */
582    public void setMarker(String key, CategoryMarker marker) {
583        CategoryMarker existing = this.markers.get(key);
584        if (existing != null) {
585            existing.removeChangeListener(this);
586        }
587        this.markers.put(key, marker);
588        if (marker != null) {
589            marker.addChangeListener(this);
590        }
591        fireChangeEvent(false);
592    } 
593
594    /**
595     * Returns a new map containing the markers that are assigned to this axis.
596     * 
597     * @return A map. 
598     * 
599     * @since 1.2
600     */
601    public Map<String, CategoryMarker> getMarkers() {
602        return new LinkedHashMap<>(this.markers);    
603    }
604    
605    /**
606     * Returns the width of a single category in the units of the axis
607     * range.
608     * 
609     * @return The width of a single category. 
610     */
611    @Override
612    public double getCategoryWidth() {
613        double length = this.range.getLength();
614        double start = this.range.getMin() + (this.lowerMargin * length);
615        double end = this.range.getMax() - (this.upperMargin * length);
616        double available = (end - start);
617        return available / this.categories.size();
618    }
619    
620    /**
621     * Configures the axis to be used as a row axis for the specified
622     * plot.  This method is for internal use, you should not call it directly.
623     * 
624     * @param plot  the plot ({@code null} not permitted).
625     */
626    @Override @SuppressWarnings("unchecked")
627    public void configureAsRowAxis(CategoryPlot3D plot) {
628        Args.nullNotPermitted(plot, "plot");
629        this.categories = plot.getDataset().getRowKeys();
630        this.isColumnAxis = false;
631        this.isRowAxis = true;
632    }
633
634    /**
635     * Configures the axis to be used as a column axis for the specified
636     * plot.  This method is for internal use, you won't normally need to call
637     * it directly.
638     * 
639     * @param plot  the plot ({@code null} not permitted).
640     */
641    @Override @SuppressWarnings("unchecked")
642    public void configureAsColumnAxis(CategoryPlot3D plot) {
643        Args.nullNotPermitted(plot, "plot");
644        this.categories = plot.getDataset().getColumnKeys();
645        this.isColumnAxis = true;
646        this.isRowAxis = false;
647    }
648
649    /**
650     * Returns the value for the specified category, or {@code Double.NaN}
651     * if the category is not registered on the axis.
652     * 
653     * @param category  the category ({@code null} not permitted).
654     * 
655     * @return The value.
656     */
657    @Override
658    public double getCategoryValue(Comparable<?> category) {
659        int index = this.categories.indexOf(category);
660        if (index < 0) {
661            return Double.NaN;
662        }
663        double length = this.range.getLength();
664        double start = this.range.getMin() + (this.lowerMargin * length);
665        double end = this.range.getMax() - (this.upperMargin * length);
666        double available = (end - start);
667        double categoryCount = this.categories.size();
668        if (categoryCount == 1) {
669            return (start + end) / 2.0;
670        }
671        if (this.firstCategoryHalfWidth) {
672            categoryCount -= 0.5;
673        }
674        if (this.lastCategoryHalfWidth) {
675            categoryCount -= 0.5;
676        }
677        double categoryWidth = 0.0;
678        if (categoryCount > 0.0) {
679            categoryWidth = available / categoryCount;
680        }
681        double adj = this.firstCategoryHalfWidth ? 0.0 : 0.5;
682        return start + (adj + index) * categoryWidth;
683    }
684    
685    /**
686     * Translates a value on the axis to the equivalent coordinate in the 
687     * 3D world used to construct a model of the chart.
688     * 
689     * @param value  the value along the axis.
690     * @param length  the length of one side of the 3D box containing the model.
691     * 
692     * @return A coordinate in 3D space.
693     */
694    @Override
695    public double translateToWorld(double value, double length) {
696        double p = getRange().percent(value, isInverted());
697        return length * p;
698    }
699
700    /**
701     * Draws the axis between the two points {@code pt0} and {@code pt1} in 
702     * Java2D space.
703     * 
704     * @param g2  the graphics target ({@code null} not permitted).
705     * @param pt0  the starting point for the axis ({@code null} not 
706     *     permitted).
707     * @param pt1  the ending point for the axis ({@code null} not 
708     *     permitted).
709     * @param opposingPt  a point on the opposite side of the line from the 
710     *         labels ({@code null} not permitted).
711     * @param tickData  the tick data, contains positioning anchors calculated 
712     *     by the 3D engine ({@code null} not permitted).
713     * @param info  an object to be populated with rendering info 
714     *     ({@code null} permitted).
715     * @param hinting  perform element hinting?
716     */
717    @Override
718    public void draw(Graphics2D g2, Point2D pt0, Point2D pt1, 
719            Point2D opposingPt, List<TickData> tickData, RenderingInfo info,
720            boolean hinting) {
721        
722        if (!isVisible()) {
723            return;
724        }
725        if (pt0.equals(pt1)) { // if the axis starts and ends on the same point
726            return;            // there is nothing we can draw
727        }
728        
729        // draw the axis line (if you want no line, setting the line color
730        // to fully transparent will achieve this)
731        g2.setStroke(getLineStroke());
732        g2.setPaint(getLineColor());
733        Line2D axisLine = new Line2D.Float(pt0, pt1);
734        g2.draw(axisLine);
735 
736        // draw the tick marks - during this pass we will also find the maximum
737        // tick label width
738        g2.setPaint(this.tickMarkPaint);
739        g2.setStroke(this.tickMarkStroke);
740        g2.setFont(getTickLabelFont());
741        double maxTickLabelWidth = 0.0;
742        for (TickData t : tickData) {
743            if (this.tickMarkLength > 0.0) {
744                Line2D tickLine = Utils2D.createPerpendicularLine(axisLine, 
745                        t.getAnchorPt(), this.tickMarkLength, opposingPt);
746                g2.draw(tickLine);
747            }
748            String tickLabel = t.getKeyLabel();
749            maxTickLabelWidth = Math.max(maxTickLabelWidth, 
750                    g2.getFontMetrics().stringWidth(tickLabel));
751        }
752
753        double maxTickLabelDim = maxTickLabelWidth;
754        if (getTickLabelsVisible()) {
755            g2.setPaint(getTickLabelColor());
756            if (this.tickLabelOrientation.equals(
757                    LabelOrientation.PERPENDICULAR)) {
758                drawPerpendicularTickLabels(g2, axisLine, opposingPt, tickData,
759                        info, hinting);
760            } else if (this.tickLabelOrientation.equals(
761                    LabelOrientation.PARALLEL)) {
762                maxTickLabelDim = drawParallelTickLabels(g2, axisLine, 
763                        opposingPt, tickData, maxTickLabelWidth, info, hinting);
764            }
765        } else {
766            maxTickLabelDim = 0.0;
767        }
768
769        // draw the axis label if there is one
770        if (getLabel() != null) {
771            Shape labelBounds = drawAxisLabel(getLabel(), g2, axisLine, 
772                    opposingPt, maxTickLabelDim + this.tickMarkLength 
773                    + this.tickLabelOffset + getLabelOffset(), info, hinting);
774        }
775    }
776    
777    /**
778     * Returns "row" if the axis has been configured as a row axis, "column" if
779     * the axis has been configured as a column axis, and the empty string ("")
780     * if the axis has not yet been configured.
781     * 
782     * @return A string (never {@code null}).
783     * 
784     * @since 1.3
785     */
786    @Override
787    protected String axisStr() {
788        String result = "";
789        if (this.isRowAxis) {
790            result = "row";
791        } else if (this.isColumnAxis) {
792            result = "column";
793        }
794        return result;
795    }
796    
797    private double drawParallelTickLabels(Graphics2D g2, Line2D axisLine,
798            Point2D opposingPt, List<TickData> tickData, 
799            double maxTickLabelWidth, RenderingInfo info, boolean hinting) {
800        int levels = 1;
801        LineMetrics lm = g2.getFontMetrics().getLineMetrics("123", g2);
802        double height = lm.getHeight();
803        if (tickData.size() > 1) {
804        
805            // work out how many offset levels we need to display the 
806            // categories without overlapping
807            Point2D p0 = tickData.get(0).getAnchorPt();
808            Point2D pN = tickData.get(tickData.size() - 1).getAnchorPt();
809            double availableWidth = pN.distance(p0) 
810                    * tickData.size() / (tickData.size() - 1);
811            int labelsPerLevel = (int) Math.floor(availableWidth / 
812                    (maxTickLabelWidth * tickLabelFactor));
813            int levelsRequired = this.maxTickLabelLevels;
814            if (labelsPerLevel > 0) {
815                levelsRequired = this.categories.size() / labelsPerLevel + 1;
816            }
817            levels = Math.min(levelsRequired, this.maxTickLabelLevels);
818        }
819        
820        int index = 0;
821        for (TickData t : tickData) {
822            int level = index % levels;
823            double adj = height * (level + 0.5);
824            Line2D perpLine = Utils2D.createPerpendicularLine(axisLine, 
825                    t.getAnchorPt(), this.tickMarkLength 
826                    + this.tickLabelOffset + adj, opposingPt);
827            double axisTheta = Utils2D.calculateTheta(axisLine);
828            TextAnchor textAnchor = TextAnchor.CENTER;
829            if (axisTheta >= Math.PI / 2.0) {
830                axisTheta = axisTheta - Math.PI;
831            } else if (axisTheta <= -Math.PI / 2) {
832                axisTheta = axisTheta + Math.PI;  
833            }
834            String tickLabel = t.getKeyLabel();
835            if (hinting) {
836                Map<String, String> m = new HashMap<>();
837                m.put("ref", "{\"type\": \"categoryTickLabel\", \"axis\": \"" 
838                        + axisStr() + "\", \"key\": \"" 
839                        + t.getKey() + "\"}");
840                g2.setRenderingHint(Chart3DHints.KEY_BEGIN_ELEMENT, m);
841            }
842
843            Shape bounds = TextUtils.drawRotatedString(tickLabel, g2, 
844                    (float) perpLine.getX2(), (float) perpLine.getY2(), 
845                    textAnchor, axisTheta, textAnchor);
846            if (hinting) {
847                g2.setRenderingHint(Chart3DHints.KEY_END_ELEMENT, true);
848            }
849            if (info != null) {
850                RenderedElement tickLabelElement = new RenderedElement(
851                        InteractiveElementType.CATEGORY_AXIS_TICK_LABEL, bounds);
852                tickLabelElement.setProperty("label", tickLabel);
853                tickLabelElement.setProperty("axis", axisStr());
854                info.addOffsetElement(tickLabelElement);
855            }
856            index++;
857        }
858        return height * levels;
859    }
860    
861    /**
862     * Draws the category labels perpendicular to the axis.
863     * 
864     * @param g2  the graphics target.
865     * @param axisLine  the axis line.
866     * @param opposingPt  an opposing point (used to indicate which side the
867     *     labels will appear on).
868     * @param tickData  the tick data.
869     * @param info  if not {@code null} this will be populated with 
870     *     {@link RenderedElement} instances for the tick labels.
871     * @param hinting  
872     */
873    @SuppressWarnings("unchecked")
874    private void drawPerpendicularTickLabels(Graphics2D g2, Line2D axisLine,
875            Point2D opposingPt, List<TickData> tickData, RenderingInfo info,
876            boolean hinting) {
877        
878        for (TickData t : tickData) {
879            Line2D perpLine = Utils2D.createPerpendicularLine(axisLine, 
880                    t.getAnchorPt(), this.tickMarkLength 
881                    + this.tickLabelOffset, opposingPt);
882            double perpTheta = Utils2D.calculateTheta(perpLine);
883            TextAnchor textAnchor = TextAnchor.CENTER_LEFT;
884            if (perpTheta >= Math.PI / 2.0) {
885                perpTheta = perpTheta - Math.PI;
886                textAnchor = TextAnchor.CENTER_RIGHT;
887            } else if (perpTheta <= -Math.PI / 2) {
888                perpTheta = perpTheta + Math.PI;
889                textAnchor = TextAnchor.CENTER_RIGHT;   
890            }
891            String tickLabel = t.getKeyLabel();
892            if (hinting) {
893                Map m = new HashMap<String, String>();
894                m.put("ref", "{\"type\": \"categoryAxisLabel\", \"axis\": \"" 
895                        + axisStr() + "\", \"key\": \"" 
896                        + t.getKey() + "\"}");
897                g2.setRenderingHint(Chart3DHints.KEY_BEGIN_ELEMENT, m);
898            }
899            Shape bounds = TextUtils.drawRotatedString(tickLabel, g2, 
900                    (float) perpLine.getX2(), (float) perpLine.getY2(), 
901                    textAnchor, perpTheta, textAnchor);
902            if (hinting) {
903                g2.setRenderingHint(Chart3DHints.KEY_END_ELEMENT, true);
904            }
905            if (info != null) {
906                RenderedElement tickLabelElement = new RenderedElement(
907                        InteractiveElementType.CATEGORY_AXIS_TICK_LABEL, bounds);
908                tickLabelElement.setProperty("label", tickLabel);
909                tickLabelElement.setProperty("axis", axisStr());
910                info.addOffsetElement(tickLabelElement);
911            }
912        }
913    }
914    
915    /**
916     * Generates the tick data for the axis (assumes the axis is being used
917     * as the row axis).  The dataset is passed as an argument to provide the 
918     * opportunity to incorporate dataset-specific info into tick labels (for 
919     * example, a row label might show the total for that row in the dataset)
920     * ---whether or not this is used depends on the axis implementation.
921     * 
922     * @param dataset  the dataset ({@code null} not permitted).
923     * 
924     * @return The tick data.
925     * 
926     * @since 1.2
927     */
928    @Override @SuppressWarnings("unchecked")
929    public List<TickData> generateTickDataForRows(CategoryDataset3D dataset) {
930        Args.nullNotPermitted(dataset, "dataset");
931        List<TickData> result = new ArrayList<>(this.categories.size());
932        for (Comparable<?> key : this.categories) {
933            double pos = this.range.percent(getCategoryValue(key));
934            String label = this.tickLabelGenerator.generateRowLabel(dataset, 
935                    key);
936            result.add(new TickData(pos, key, label));
937        }
938        return result;
939    }
940
941    /**
942     * Generates the tick data for the axis (assumes the axis is being used
943     * as the row axis).  The dataset is passed as an argument to provide the 
944     * opportunity to incorporate dataset-specific info into tick labels (for 
945     * example, a row label might show the total for that row in the dataset)
946     * ---whether or not this is used depends on the axis implementation.
947     * 
948     * @param dataset  the dataset ({@code null} not permitted).
949     * 
950     * @return The tick data.
951     * 
952     * @since 1.2
953     */
954    @Override @SuppressWarnings("unchecked")
955    public List<TickData> generateTickDataForColumns(
956            CategoryDataset3D dataset) {
957        Args.nullNotPermitted(dataset, "dataset");
958        List<TickData> result = new ArrayList<>(this.categories.size());
959        for (Comparable<?> key : this.categories) {
960            double pos = this.range.percent(getCategoryValue(key));
961            String label = this.tickLabelGenerator.generateColumnLabel(dataset, 
962                    key);
963            result.add(new TickData(pos, key, label));
964        }
965        return result;
966    }
967
968    /**
969     * Generates and returns a list of marker data items for the axis.
970     * 
971     * @return A list of marker data items (never {@code null}). 
972     */
973    @Override
974    public List<MarkerData> generateMarkerData() {
975        List<MarkerData> result = new ArrayList<>();
976        for (Map.Entry<String, CategoryMarker> entry 
977                : this.markers.entrySet()) {
978            CategoryMarker cm = entry.getValue();
979            if (cm == null) {
980                continue;
981            }
982            MarkerData markerData;
983            if (cm.getType().equals(CategoryMarkerType.LINE)) {
984                double pos = getCategoryValue(cm.getCategory());
985                markerData = new MarkerData(entry.getKey(), pos);
986                markerData.setLabelAnchor(cm.getLabel() != null 
987                            ? cm.getLabelAnchor() : null);
988            } else if (cm.getType().equals(CategoryMarkerType.BAND)) {
989                double pos = getCategoryValue(cm.getCategory());
990                double width = getCategoryWidth();                
991                markerData = new MarkerData(entry.getKey(), pos - width / 2, 
992                        false, pos + width / 2, false);
993                markerData.setLabelAnchor(cm.getLabel() != null 
994                            ? cm.getLabelAnchor() : null);
995            } else {
996                throw new RuntimeException("Unrecognised marker: " 
997                        + cm.getType());
998            }
999            result.add(markerData);
1000        }
1001        return result;
1002    }
1003
1004    /**
1005     * Receives a {@link ChartElementVisitor}.  This method is part of a general
1006     * mechanism for traversing the chart structure and performing operations
1007     * on each element in the chart.  You will not normally call this method
1008     * directly.
1009     * 
1010     * @param visitor  the visitor ({@code null} not permitted).
1011     * 
1012     * @since 1.2
1013     */
1014    @Override
1015    public void receive(ChartElementVisitor visitor) {
1016        for (Marker marker : this.markers.values()) {
1017            marker.receive(visitor);
1018        }
1019        visitor.visit(this);
1020    }
1021
1022    /**
1023     * Tests this instance for equality with an arbitrary object.
1024     * 
1025     * @param obj  the object to test against ({@code null} not permitted).
1026     * 
1027     * @return A boolean.
1028     */
1029    @Override
1030    public boolean equals(Object obj) {
1031        if (obj == this) {
1032            return true;
1033        }
1034        if (!(obj instanceof StandardCategoryAxis3D)) {
1035            return false;
1036        }
1037        StandardCategoryAxis3D that = (StandardCategoryAxis3D) obj;
1038        if (this.lowerMargin != that.lowerMargin) {
1039            return false;
1040        }
1041        if (this.upperMargin != that.upperMargin) {
1042            return false;
1043        }
1044        if (this.firstCategoryHalfWidth != that.firstCategoryHalfWidth) {
1045            return false;
1046        }
1047        if (this.lastCategoryHalfWidth != that.lastCategoryHalfWidth) {
1048            return false;
1049        }
1050        if (this.tickMarkLength != that.tickMarkLength) {
1051            return false;
1052        }
1053        if (!ObjectUtils.equalsPaint(this.tickMarkPaint, that.tickMarkPaint)) {
1054            return false;            
1055        }
1056        if (!this.tickMarkStroke.equals(that.tickMarkStroke)) {
1057            return false;
1058        }
1059        if (!this.tickLabelGenerator.equals(that.tickLabelGenerator)) {
1060            return false;
1061        }
1062        if (this.tickLabelOffset != that.tickLabelOffset) {
1063            return false;
1064        }
1065        if (!this.tickLabelOrientation.equals(that.tickLabelOrientation)) {
1066            return false;
1067        }
1068        if (this.tickLabelFactor != that.tickLabelFactor) {
1069            return false;
1070        }
1071        if (this.maxTickLabelLevels != that.maxTickLabelLevels) {
1072            return false;
1073        }
1074        if (!this.markers.equals(that.markers)) {
1075            return false;
1076        }
1077        return super.equals(obj);
1078    }
1079
1080    /**
1081     * Provides serialization support.
1082     *
1083     * @param stream  the output stream.
1084     *
1085     * @throws IOException  if there is an I/O error.
1086     */
1087    private void writeObject(ObjectOutputStream stream) throws IOException {
1088        stream.defaultWriteObject();
1089        SerialUtils.writePaint(this.tickMarkPaint, stream);
1090        SerialUtils.writeStroke(this.tickMarkStroke, stream);
1091    }
1092
1093    /**
1094     * Provides serialization support.
1095     *
1096     * @param stream  the input stream.
1097     *
1098     * @throws IOException  if there is an I/O error.
1099     * @throws ClassNotFoundException  if there is a classpath problem.
1100     */
1101    private void readObject(ObjectInputStream stream)
1102        throws IOException, ClassNotFoundException {
1103        stream.defaultReadObject();
1104        this.tickMarkPaint = SerialUtils.readPaint(stream);
1105        this.tickMarkStroke = SerialUtils.readStroke(stream);
1106    }
1107    
1108    /**
1109     * Returns {@code true} if the axis inverts the order of the data items,
1110     * and {@code false} otherwise.
1111     * 
1112     * @return A boolean.
1113     * 
1114     * @since 1.5
1115     */
1116    @Override
1117    public boolean isInverted() {
1118        return this.inverted;
1119    }
1120    
1121    /**
1122     * Sets the flag that controls whether or not the axis inverts the order
1123     * of the data items and sends an {@link Axis3DChangeEvent} to all 
1124     * registered listeners.
1125     * 
1126     * @param inverted  the new flag value. 
1127     * 
1128     * @since 1.5
1129     */
1130    @Override
1131    public void setInverted(boolean inverted) {
1132        this.inverted = inverted;
1133        fireChangeEvent(true);
1134    }
1135}