001/* ===========================================================
002 * Orson Charts : a 3D chart library for the Java(tm) platform
003 * ===========================================================
004 * 
005 * (C)opyright 2013-2016, by Object Refinery Limited.  All rights reserved.
006 * 
007 * http://www.object-refinery.com/orsoncharts/index.html
008 * 
009 * This program is free software: you can redistribute it and/or modify
010 * it under the terms of the GNU General Public License as published by
011 * the Free Software Foundation, either version 3 of the License, or
012 * (at your option) any later version.
013 *
014 * This program is distributed in the hope that it will be useful,
015 * but WITHOUT ANY WARRANTY; without even the implied warranty of
016 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
017 * GNU General Public License for more details.
018 *
019 * You should have received a copy of the GNU General Public License
020 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
021 * 
022 * [Oracle and Java are registered trademarks of Oracle and/or its affiliates. 
023 * Other names may be trademarks of their respective owners.]
024 * 
025 * If you do not wish to be bound by the terms of the GPL, an alternative
026 * commercial license can be purchased.  For details, please see visit the
027 * Orson Charts home page:
028 * 
029 * http://www.object-refinery.com/orsoncharts/index.html
030 * 
031 */
032
033package com.orsoncharts.axis;
034
035import java.awt.BasicStroke;
036import java.awt.Color;
037import java.awt.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 com.orsoncharts.Chart3DHints;
055import com.orsoncharts.ChartElementVisitor;
056import com.orsoncharts.Range;
057import com.orsoncharts.data.category.CategoryDataset3D;
058import com.orsoncharts.graphics3d.RenderedElement;
059import com.orsoncharts.graphics3d.RenderingInfo;
060import com.orsoncharts.graphics3d.Utils2D;
061import com.orsoncharts.interaction.InteractiveElementType;
062import com.orsoncharts.label.CategoryLabelGenerator;
063import com.orsoncharts.label.StandardCategoryLabelGenerator;
064import com.orsoncharts.marker.CategoryMarker;
065import com.orsoncharts.marker.CategoryMarkerType;
066import com.orsoncharts.marker.Marker;
067import com.orsoncharts.marker.MarkerData;
068import com.orsoncharts.plot.CategoryPlot3D;
069import com.orsoncharts.renderer.category.AreaRenderer3D;
070import com.orsoncharts.util.ArgChecks;
071import com.orsoncharts.util.ObjectUtils;
072import com.orsoncharts.util.SerialUtils;
073import com.orsoncharts.util.TextAnchor;
074import com.orsoncharts.util.TextUtils;
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<Comparable<?>>();
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<String, CategoryMarker>();
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        ArgChecks.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        ArgChecks.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        ArgChecks.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        ArgChecks.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        ArgChecks.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<String, CategoryMarker>(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
627    public void configureAsRowAxis(CategoryPlot3D plot) {
628        ArgChecks.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
642    public void configureAsColumnAxis(CategoryPlot3D plot) {
643        ArgChecks.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 m = new HashMap<String, String>();
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    private void drawPerpendicularTickLabels(Graphics2D g2, Line2D axisLine,
874            Point2D opposingPt, List<TickData> tickData, RenderingInfo info,
875            boolean hinting) {
876        
877        for (TickData t : tickData) {
878            Line2D perpLine = Utils2D.createPerpendicularLine(axisLine, 
879                    t.getAnchorPt(), this.tickMarkLength 
880                    + this.tickLabelOffset, opposingPt);
881            double perpTheta = Utils2D.calculateTheta(perpLine);
882            TextAnchor textAnchor = TextAnchor.CENTER_LEFT;
883            if (perpTheta >= Math.PI / 2.0) {
884                perpTheta = perpTheta - Math.PI;
885                textAnchor = TextAnchor.CENTER_RIGHT;
886            } else if (perpTheta <= -Math.PI / 2) {
887                perpTheta = perpTheta + Math.PI;
888                textAnchor = TextAnchor.CENTER_RIGHT;   
889            }
890            String tickLabel = t.getKeyLabel();
891            if (hinting) {
892                Map m = new HashMap<String, String>();
893                m.put("ref", "{\"type\": \"categoryAxisLabel\", \"axis\": \"" 
894                        + axisStr() + "\", \"key\": \"" 
895                        + t.getKey() + "\"}");
896                g2.setRenderingHint(Chart3DHints.KEY_BEGIN_ELEMENT, m);
897            }
898            Shape bounds = TextUtils.drawRotatedString(tickLabel, g2, 
899                    (float) perpLine.getX2(), (float) perpLine.getY2(), 
900                    textAnchor, perpTheta, textAnchor);
901            if (hinting) {
902                g2.setRenderingHint(Chart3DHints.KEY_END_ELEMENT, true);
903            }
904            if (info != null) {
905                RenderedElement tickLabelElement = new RenderedElement(
906                        InteractiveElementType.CATEGORY_AXIS_TICK_LABEL, bounds);
907                tickLabelElement.setProperty("label", tickLabel);
908                tickLabelElement.setProperty("axis", axisStr());
909                info.addOffsetElement(tickLabelElement);
910            }
911        }
912    }
913    
914    /**
915     * Generates the tick data for the axis (assumes the axis is being used
916     * as the row axis).  The dataset is passed as an argument to provide the 
917     * opportunity to incorporate dataset-specific info into tick labels (for 
918     * example, a row label might show the total for that row in the dataset)
919     * ---whether or not this is used depends on the axis implementation.
920     * 
921     * @param dataset  the dataset ({@code null} not permitted).
922     * 
923     * @return The tick data.
924     * 
925     * @since 1.2
926     */
927    @Override
928    public List<TickData> generateTickDataForRows(CategoryDataset3D dataset) {
929        ArgChecks.nullNotPermitted(dataset, "dataset");
930        List<TickData> result = new ArrayList<TickData>(this.categories.size());
931        for (Comparable<?> key : this.categories) {
932            double pos = this.range.percent(getCategoryValue(key));
933            String label = this.tickLabelGenerator.generateRowLabel(dataset, 
934                    key);
935            result.add(new TickData(pos, key, label));
936        }
937        return result;
938    }
939
940    /**
941     * Generates the tick data for the axis (assumes the axis is being used
942     * as the row axis).  The dataset is passed as an argument to provide the 
943     * opportunity to incorporate dataset-specific info into tick labels (for 
944     * example, a row label might show the total for that row in the dataset)
945     * ---whether or not this is used depends on the axis implementation.
946     * 
947     * @param dataset  the dataset ({@code null} not permitted).
948     * 
949     * @return The tick data.
950     * 
951     * @since 1.2
952     */
953    @Override
954    public List<TickData> generateTickDataForColumns(
955            CategoryDataset3D dataset) {
956        ArgChecks.nullNotPermitted(dataset, "dataset");
957        List<TickData> result = new ArrayList<TickData>(this.categories.size());
958        for (Comparable<?> key : this.categories) {
959            double pos = this.range.percent(getCategoryValue(key));
960            String label = this.tickLabelGenerator.generateColumnLabel(dataset, 
961                    key);
962            result.add(new TickData(pos, key, label));
963        }
964        return result;
965    }
966
967    /**
968     * Generates and returns a list of marker data items for the axis.
969     * 
970     * @return A list of marker data items (never {@code null}). 
971     */
972    @Override
973    public List<MarkerData> generateMarkerData() {
974        List<MarkerData> result = new ArrayList<MarkerData>();
975        for (Map.Entry<String, CategoryMarker> entry 
976                : this.markers.entrySet()) {
977            CategoryMarker cm = entry.getValue();
978            if (cm == null) {
979                continue;
980            }
981            MarkerData markerData;
982            if (cm.getType().equals(CategoryMarkerType.LINE)) {
983                double pos = getCategoryValue(cm.getCategory());
984                markerData = new MarkerData(entry.getKey(), pos);
985                markerData.setLabelAnchor(cm.getLabel() != null 
986                            ? cm.getLabelAnchor() : null);
987            } else if (cm.getType().equals(CategoryMarkerType.BAND)) {
988                double pos = getCategoryValue(cm.getCategory());
989                double width = getCategoryWidth();                
990                markerData = new MarkerData(entry.getKey(), pos - width / 2, 
991                        false, pos + width / 2, false);
992                markerData.setLabelAnchor(cm.getLabel() != null 
993                            ? cm.getLabelAnchor() : null);
994            } else {
995                throw new RuntimeException("Unrecognised marker: " 
996                        + cm.getType());
997            }
998            result.add(markerData);
999        }
1000        return result;
1001    }
1002
1003    /**
1004     * Receives a {@link ChartElementVisitor}.  This method is part of a general
1005     * mechanism for traversing the chart structure and performing operations
1006     * on each element in the chart.  You will not normally call this method
1007     * directly.
1008     * 
1009     * @param visitor  the visitor ({@code null} not permitted).
1010     * 
1011     * @since 1.2
1012     */
1013    @Override
1014    public void receive(ChartElementVisitor visitor) {
1015        for (Marker marker : this.markers.values()) {
1016            marker.receive(visitor);
1017        }
1018        visitor.visit(this);
1019    }
1020
1021    /**
1022     * Tests this instance for equality with an arbitrary object.
1023     * 
1024     * @param obj  the object to test against ({@code null} not permitted).
1025     * 
1026     * @return A boolean.
1027     */
1028    @Override
1029    public boolean equals(Object obj) {
1030        if (obj == this) {
1031            return true;
1032        }
1033        if (!(obj instanceof StandardCategoryAxis3D)) {
1034            return false;
1035        }
1036        StandardCategoryAxis3D that = (StandardCategoryAxis3D) obj;
1037        if (this.lowerMargin != that.lowerMargin) {
1038            return false;
1039        }
1040        if (this.upperMargin != that.upperMargin) {
1041            return false;
1042        }
1043        if (this.firstCategoryHalfWidth != that.firstCategoryHalfWidth) {
1044            return false;
1045        }
1046        if (this.lastCategoryHalfWidth != that.lastCategoryHalfWidth) {
1047            return false;
1048        }
1049        if (this.tickMarkLength != that.tickMarkLength) {
1050            return false;
1051        }
1052        if (!ObjectUtils.equalsPaint(this.tickMarkPaint, that.tickMarkPaint)) {
1053            return false;            
1054        }
1055        if (!this.tickMarkStroke.equals(that.tickMarkStroke)) {
1056            return false;
1057        }
1058        if (!this.tickLabelGenerator.equals(that.tickLabelGenerator)) {
1059            return false;
1060        }
1061        if (this.tickLabelOffset != that.tickLabelOffset) {
1062            return false;
1063        }
1064        if (!this.tickLabelOrientation.equals(that.tickLabelOrientation)) {
1065            return false;
1066        }
1067        if (this.tickLabelFactor != that.tickLabelFactor) {
1068            return false;
1069        }
1070        if (this.maxTickLabelLevels != that.maxTickLabelLevels) {
1071            return false;
1072        }
1073        if (!this.markers.equals(that.markers)) {
1074            return false;
1075        }
1076        return super.equals(obj);
1077    }
1078
1079    /**
1080     * Provides serialization support.
1081     *
1082     * @param stream  the output stream.
1083     *
1084     * @throws IOException  if there is an I/O error.
1085     */
1086    private void writeObject(ObjectOutputStream stream) throws IOException {
1087        stream.defaultWriteObject();
1088        SerialUtils.writePaint(this.tickMarkPaint, stream);
1089        SerialUtils.writeStroke(this.tickMarkStroke, stream);
1090    }
1091
1092    /**
1093     * Provides serialization support.
1094     *
1095     * @param stream  the input stream.
1096     *
1097     * @throws IOException  if there is an I/O error.
1098     * @throws ClassNotFoundException  if there is a classpath problem.
1099     */
1100    private void readObject(ObjectInputStream stream)
1101        throws IOException, ClassNotFoundException {
1102        stream.defaultReadObject();
1103        this.tickMarkPaint = SerialUtils.readPaint(stream);
1104        this.tickMarkStroke = SerialUtils.readStroke(stream);
1105    }
1106    
1107    /**
1108     * Returns {@code true} if the axis inverts the order of the data items,
1109     * and {@code false} otherwise.
1110     * 
1111     * @return A boolean.
1112     * 
1113     * @since 1.5
1114     */
1115    @Override
1116    public boolean isInverted() {
1117        return this.inverted;
1118    }
1119    
1120    /**
1121     * Sets the flag that controls whether or not the axis inverts the order
1122     * of the data items and sends an {@link Axis3DChangeEvent} to all 
1123     * registered listeners.
1124     * 
1125     * @param inverted  the new flag value. 
1126     * 
1127     * @since 1.5
1128     */
1129    public void setInverted(boolean inverted) {
1130        this.inverted = inverted;
1131        fireChangeEvent(true);
1132    }
1133}