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;
034
035import java.awt.BasicStroke;
036import java.awt.Stroke;
037import java.awt.geom.Line2D;
038import java.awt.Color;
039import java.awt.Graphics2D;
040import java.awt.Font;
041import java.awt.Paint;
042import java.awt.RenderingHints;
043import java.awt.Shape;
044import java.awt.geom.AffineTransform;
045import java.awt.geom.Path2D;
046import java.awt.geom.Point2D;
047import java.awt.geom.Dimension2D;
048import java.awt.geom.Rectangle2D;
049import java.io.Serializable;
050import java.io.IOException;
051import java.io.ObjectInputStream;
052import java.util.ArrayList;
053import java.util.Collections;
054import java.util.List;
055import java.util.HashMap;
056import java.util.Map;
057
058import javax.swing.event.EventListenerList;
059
060import com.orsoncharts.ChartBox3D.ChartBoxFace;
061import com.orsoncharts.axis.Axis3D;
062import com.orsoncharts.axis.TickData;
063import com.orsoncharts.axis.ValueAxis3D;
064import com.orsoncharts.data.ItemKey;
065import com.orsoncharts.graphics3d.Dimension3D;
066import com.orsoncharts.graphics3d.DoubleSidedFace;
067import com.orsoncharts.graphics3d.Drawable3D;
068import com.orsoncharts.graphics3d.Face;
069import com.orsoncharts.graphics3d.FaceSorter;
070import com.orsoncharts.graphics3d.LabelFace;
071import com.orsoncharts.graphics3d.Object3D;
072import com.orsoncharts.graphics3d.Point3D;
073import com.orsoncharts.graphics3d.Utils2D;
074import com.orsoncharts.graphics3d.ViewPoint3D;
075import com.orsoncharts.graphics3d.World;
076import com.orsoncharts.legend.LegendAnchor;
077import com.orsoncharts.plot.CategoryPlot3D;
078import com.orsoncharts.plot.PiePlot3D;
079import com.orsoncharts.plot.Plot3DChangeEvent;
080import com.orsoncharts.plot.Plot3DChangeListener;
081import com.orsoncharts.plot.Plot3D;
082import com.orsoncharts.plot.XYZPlot;
083import com.orsoncharts.graphics3d.Offset2D;
084import com.orsoncharts.graphics3d.RenderingInfo;
085import com.orsoncharts.graphics3d.StandardFaceSorter;
086import com.orsoncharts.graphics3d.RenderedElement;
087import com.orsoncharts.interaction.InteractiveElementType;
088import com.orsoncharts.legend.LegendBuilder;
089import com.orsoncharts.legend.StandardLegendBuilder;
090import com.orsoncharts.marker.Marker;
091import com.orsoncharts.marker.MarkerData;
092import com.orsoncharts.style.ChartStyle;
093import com.orsoncharts.style.ChartStyleChangeEvent;
094import com.orsoncharts.style.ChartStyleChangeListener;
095import com.orsoncharts.style.ChartStyler;
096import com.orsoncharts.table.GradientRectanglePainter;
097import com.orsoncharts.table.GridElement;
098import com.orsoncharts.table.HAlign;
099import com.orsoncharts.table.TableElement;
100import com.orsoncharts.table.TextElement;
101import com.orsoncharts.table.StandardRectanglePainter;
102import com.orsoncharts.table.RectanglePainter;
103import com.orsoncharts.util.Anchor2D;
104import com.orsoncharts.util.ArgChecks;
105import com.orsoncharts.util.ObjectUtils;
106import com.orsoncharts.util.Orientation;
107import com.orsoncharts.util.RefPt2D;
108import com.orsoncharts.util.TextUtils;
109import com.orsoncharts.util.TextAnchor;
110
111/**
112 * A chart object for 3D charts (this is the umbrella object that manages all
113 * the components of the chart).  The {@link Chart3DFactory} class provides 
114 * some factory methods to construct common types of charts.
115 * <br><br>
116 * All rendering is done via the Java2D API, so this object is able to draw to 
117 * any implementation of the Graphics2D API (including 
118 * <a href="http://www.jfree.org/jfreesvg/" target="JFreeSVG">JFreeSVG</a> for 
119 * SVG output, and 
120 * <a href="http://www.object-refinery.com/pdf/" target="OrsonPDF">OrsonPDF</a> 
121 * for PDF output).
122 * <br><br>
123 * In the step prior to rendering, a chart is composed in a 3D model that is
124 * referred to as the "world".  The dimensions of this 3D model are measured
125 * in "world units" and the overall size is controlled by the plot.  You will 
126 * see some attributes in the API that are specified in "world units", and these
127 * can be used to modify how objects are composed within the 3D world model.  
128 * Once the objects (for example, bars in a bar chart) within the world have 
129 * been composed, they are projected onto a 2D plane and rendered to the 
130 * {@code Graphics2D} target (such as the screen, image, SVG file or 
131 * PDF file).  
132 * <br><br>
133 * Charts can have simple titles or composite titles (anything that can be
134 * constructed as a {@link TableElement} instance.  The {@link TitleUtils}
135 * class contains methods to create a common title/subtitle composite title. 
136 * This is illustrated in some of the demo applications.  The chart title
137 * and legend (and also the axis labels) are not part of the 3D world model,
138 * they are overlaid on the output after the 3D components have been
139 * rendered.
140 * <br><br>
141 * NOTE: This class is serializable, but the serialization format is subject 
142 * to change in future releases and should not be relied upon for persisting 
143 * instances of this class.
144 * 
145 * @see Chart3DFactory
146 * @see Chart3DPanel
147 */
148@SuppressWarnings("serial")
149public class Chart3D implements Drawable3D, ChartElement, 
150        Plot3DChangeListener, ChartStyleChangeListener, Serializable {
151    
152    /** 
153     * The default projection distance. 
154     * 
155     * @since 1.2
156     */
157    public static final double DEFAULT_PROJ_DIST = 1500.0;
158    
159    /**
160     * The key for a property that stores the interactive element type.
161     * 
162     * @since 1.3
163     */
164    public static final String INTERACTIVE_ELEMENT_TYPE 
165            = "interactive_element_type";
166    
167    /**
168     * The key for a property that stores the series key.  This is used to
169     * store the series key on the {@link TableElement} representing a legend 
170     * item, and also on a corresponding {@link RenderedElement} after
171     * chart rendering (in the {@link RenderingInfo}).
172     * 
173     * @since 1.3
174     */
175    public static final String SERIES_KEY = "series_key";
176    
177    /** The chart id. */
178    private String id;
179    
180    /** A background rectangle painter, if any. */
181    private RectanglePainter background;
182    
183    /** The chart title (can be {@code null}). */
184    private TableElement title;
185    
186    /** The anchor point for the title (never {@code null}). */
187    private Anchor2D titleAnchor;
188    
189    /** A builder for the chart legend (can be {@code null}). */
190    private LegendBuilder legendBuilder;
191    
192    /** The anchor point for the legend (never {@code null}). */
193    private Anchor2D legendAnchor;
194    
195    /** The orientation for the legend (never {@code null}). */
196    private Orientation legendOrientation;
197    
198    /** The plot. */
199    private Plot3D plot;
200    
201    /** The view point. */
202    private ViewPoint3D viewPoint;
203
204    /** The projection distance. */
205    private double projDist;
206    
207    /** The chart box color (never {@code null}). */
208    private Color chartBoxColor;
209
210    /** 
211     * A translation factor applied to the chart when drawing.  We use this
212     * to allow the user (optionally) to drag the chart from its center 
213     * location to better align it with the chart title and legend.
214     */
215    private Offset2D translate2D;
216    
217    /** Storage for registered change listeners. */
218    private transient EventListenerList listenerList;
219
220    /**
221     * A flag that controls whether or not the chart will notify listeners
222     * of changes (defaults to {@code true}, but sometimes it is useful 
223     * to disable this).
224     */
225    private boolean notify;
226
227    /**
228     * Rendering hints that will be used for chart drawing.  This can be
229     * empty but it should never be {@code null}.
230     * 
231     * @since 1.1
232     */
233    private transient RenderingHints renderingHints;
234
235    /** 
236     * The chart style.
237     * 
238     * @since 1.2
239     */
240    private ChartStyle style;
241    
242    /** A 3D model of the world (represents the chart). */
243    private transient World world;
244
245    /** An object that sorts faces for rendering (painter's algorithm). */
246    private FaceSorter faceSorter;
247
248    /**
249     * A flag that controls whether or not element hints are added to the
250     * {@code Graphics2D} output.
251     */
252    private boolean elementHinting;
253    
254    /**
255     * Creates a 3D chart for the specified plot using the default chart
256     * style.  Note that a plot instance must be used in one chart instance
257     * only.
258     * 
259     * @param title  the chart title ({@code null} permitted).
260     * @param subtitle  the chart subtitle ({@code null} permitted).
261     * @param plot  the plot ({@code null} not permitted).
262     * 
263     * @see Chart3DFactory
264     */
265    public Chart3D(String title, String subtitle, Plot3D plot) {
266        this(title, subtitle, plot, Chart3DFactory.getDefaultChartStyle());
267    }
268    
269    /**
270     * Creates a 3D chart for the specified plot using the supplied style.
271     * 
272     * @param title  the chart title ({@code null} permitted).
273     * @param subtitle  the chart subtitle ({@code null} permitted).
274     * @param plot  the plot ({@code null} not permitted).
275     * @param style  the chart style ({@code null} not permitted).
276     * 
277     * @since 1.2
278     */
279    public Chart3D(String title, String subtitle, Plot3D plot, 
280            ChartStyle style) {
281        ArgChecks.nullNotPermitted(plot, "plot");
282        ArgChecks.nullNotPermitted(style, "style");
283        plot.setChart(this);
284        this.background = new StandardRectanglePainter(Color.WHITE);
285        if (title != null) {
286            this.title = TitleUtils.createTitle(title, subtitle);
287        }
288        this.titleAnchor = TitleAnchor.TOP_LEFT;
289        this.legendBuilder = new StandardLegendBuilder();
290        this.legendAnchor = LegendAnchor.BOTTOM_RIGHT;
291        this.legendOrientation = Orientation.HORIZONTAL;
292        this.plot = plot;
293        this.plot.addChangeListener(this);
294        Dimension3D dim = this.plot.getDimensions();
295        float distance = (float) dim.getDiagonalLength() * 3.0f;
296        this.viewPoint = ViewPoint3D.createAboveViewPoint(distance);
297        this.projDist = DEFAULT_PROJ_DIST;
298        this.chartBoxColor = new Color(255, 255, 255, 100);
299        this.translate2D = new Offset2D();
300        this.faceSorter = new StandardFaceSorter();
301        this.renderingHints = new RenderingHints(
302                RenderingHints.KEY_ANTIALIASING,
303                RenderingHints.VALUE_ANTIALIAS_ON);
304        this.renderingHints.put(RenderingHints.KEY_TEXT_ANTIALIASING,
305                RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
306        this.elementHinting = false;
307        this.notify = true;
308        this.listenerList = new EventListenerList();
309        this.style = style;
310        this.style.addChangeListener(this);
311        receive(new ChartStyler(this.style));
312    }
313
314    /**
315     * Returns the chart id.
316     * 
317     * @return The chart id (possibly {@code null}).
318     * 
319     * @since 1.3
320     */
321    public String getID() {
322        return this.id;
323    }
324    
325    /**
326     * Sets the chart id.
327     * 
328     * @param id  the id ({@code null} permitted).
329     * 
330     * @since 1.3
331     */
332    public void setID(String id) {
333        this.id = id;
334    }
335    
336    /**
337     * Returns the background painter (an object that is responsible for filling
338     * the background area before charts are rendered).  The default value
339     * is an instance of {@link StandardRectanglePainter} that paints the
340     * background white.
341     * 
342     * @return The background painter (possibly {@code null}).
343     * 
344     * @see #setBackground(com.orsoncharts.table.RectanglePainter) 
345     */
346    public RectanglePainter getBackground() {
347        return this.background;
348    }
349    
350    /**
351     * Sets the background painter and sends a {@link Chart3DChangeEvent} to
352     * all registered listeners.  A background painter is used to fill in the
353     * background of the chart before the 3D rendering takes place.  To fill
354     * the background with a color or image, you can use 
355     * {@link StandardRectanglePainter}.  To fill the background with a 
356     * gradient paint, use {@link GradientRectanglePainter}.
357     * 
358     * @param background  the background painter ({@code null} permitted).
359     * 
360     * @see #getBackground() 
361     */
362    public void setBackground(RectanglePainter background) {
363        this.background = background;
364        fireChangeEvent();
365    }
366    
367    /**
368     * Returns the chart title.  A {@link TableElement} is used for the title,
369     * since it allows a lot of flexibility in the types of title that can
370     * be displayed.
371     * 
372     * @return The chart title (possibly {@code null}). 
373     */
374    public TableElement getTitle() {
375        return this.title;
376    }
377    
378    /**
379     * Sets the chart title and sends a {@link Chart3DChangeEvent} to all 
380     * registered listeners.  This is a convenience method that constructs
381     * the required {@link TableElement} under-the-hood.
382     * 
383     * @param title  the title ({@code null} permitted). 
384     */
385    public void setTitle(String title) {
386        if (title == null) {
387            setTitle((TableElement) null);
388        } else {
389            setTitle(title, this.style.getTitleFont(), 
390                    TitleUtils.DEFAULT_TITLE_COLOR);
391        }
392    }
393    
394    /**
395     * Sets the chart title and sends a {@link Chart3DChangeEvent} to all 
396     * registered listeners.  This is a convenience method that constructs
397     * the required {@link TableElement} under-the-hood.
398     * 
399     * @param title  the title ({@code null} not permitted). 
400     * @param font  the font ({@code null} not permitted).
401     * @param color  the foreground color ({@code null} not permitted).
402     */
403    public void setTitle(String title, Font font, Color color) {
404        // defer 'title' null check
405        ArgChecks.nullNotPermitted(font, "font");
406        ArgChecks.nullNotPermitted(color, "color");
407        TextElement te = new TextElement(title);
408        te.setTag("CHART_TITLE");
409        te.setFont(font);
410        te.setColor(color);
411        setTitle(te);
412    }
413    
414    /**
415     * Sets the chart title and sends a {@link Chart3DChangeEvent} to all 
416     * registered listeners.  You can set the title to {@code null}, in
417     * which case there will be no chart title.
418     * 
419     * @param title  the title ({@code null} permitted).
420     */
421    public void setTitle(TableElement title) {
422        this.title = title;
423        fireChangeEvent();
424    }
425
426    /**
427     * Returns the title anchor.  This controls the position of the title
428     * in the chart area. 
429     * 
430     * @return The title anchor (never {@code null}).
431     * 
432     * @see #setTitleAnchor(com.orsoncharts.util.Anchor2D) 
433     */
434    public Anchor2D getTitleAnchor() {
435        return this.titleAnchor;
436    }
437    
438    /**
439     * Sets the title anchor and sends a {@link Chart3DChangeEvent} to all
440     * registered listeners.  There is a {@link TitleAnchor} class providing
441     * some useful default anchors.
442     * 
443     * @param anchor  the anchor ({@code null} not permitted).
444     * 
445     * @see #getTitleAnchor() 
446     */
447    public void setTitleAnchor(Anchor2D anchor) {
448        ArgChecks.nullNotPermitted(anchor, "anchor");
449        this.titleAnchor = anchor;
450        fireChangeEvent();
451    }
452
453    /**
454     * Returns the plot, which manages the dataset, the axes (if any), the 
455     * renderer (if any) and other attributes related to plotting data.  The
456     * plot is specified via the constructor...there is no method to set a 
457     * new plot for the chart, instead you need to create a new chart instance.
458     *
459     * @return The plot (never {@code null}).
460     */
461    public Plot3D getPlot() {
462        return this.plot;
463    }
464    
465    /**
466     * Returns the chart box color (the chart box is the visible, open-sided 
467     * box inside which data is plotted for all charts except pie charts). 
468     * The default value is {@code Color.WHITE}.
469     * 
470     * @return The chart box color (never {@code null}). 
471     * 
472     * @see #setChartBoxColor(java.awt.Color) 
473     */
474    public Color getChartBoxColor() {
475        return this.chartBoxColor;
476    }
477    
478    /**
479     * Sets the chart box color and sends a {@link Chart3DChangeEvent} to all 
480     * registered listeners.  Bear in mind that {@link PiePlot3D} does not
481     * display a chart box, so this attribute will be ignored for pie charts.
482     * 
483     * @param color  the color ({@code null} not permitted).
484     * 
485     * @see #getChartBoxColor()
486     */
487    public void setChartBoxColor(Color color) {
488        ArgChecks.nullNotPermitted(color, "color");
489        this.chartBoxColor = color;
490        fireChangeEvent();
491    }
492    
493    /**
494     * Returns the dimensions of the 3D object.
495     * 
496     * @return The dimensions (never {@code null}). 
497     */
498    @Override
499    public Dimension3D getDimensions() {
500        return this.plot.getDimensions();
501    }
502    
503    /**
504     * Returns the view point.
505     * 
506     * @return The view point (never {@code null}). 
507     */
508    @Override
509    public ViewPoint3D getViewPoint() {
510        return this.viewPoint;
511    }
512
513    /**
514     * Sets the view point.
515     * 
516     * @param viewPoint  the view point ({@code null} not permitted). 
517     */
518    @Override
519    public void setViewPoint(ViewPoint3D viewPoint) {
520        ArgChecks.nullNotPermitted(viewPoint, "viewPoint");
521        this.viewPoint = viewPoint;
522        fireChangeEvent();
523    }    
524
525    /** 
526     * Returns the projection distance.  The default value is 
527     * {@link #DEFAULT_PROJ_DIST}, higher numbers flatten out the perspective 
528     * and reduce distortion in the projected image.
529     * 
530     * @return The projection distance.
531     * 
532     * @since 1.2
533     */
534    @Override
535    public double getProjDistance() {
536        return this.projDist;
537    }
538    
539    /**
540     * Sets the projection distance and sends a change event to all registered
541     * listeners.
542     * 
543     * @param dist  the distance.
544     * 
545     * @since 1.2
546     */
547    @Override
548    public void setProjDistance(double dist) {
549        this.projDist = dist;
550        fireChangeEvent();
551    }
552
553    /**
554     * Sets the offset in 2D-space for the rendering of the chart.  The 
555     * default value is {@code (0, 0)} but the user can modify it via
556     * ALT-mouse-drag in the chart panel, providing an easy way to get improved
557     * chart alignment in the panels (especially prior to export to PNG, SVG or
558     * PDF).
559     * 
560     * @return The offset (never {@code null}). 
561     */
562    @Override
563    public Offset2D getTranslate2D() {
564        return this.translate2D;    
565    }
566    
567    /**
568     * Sets the offset in 2D-space for the rendering of the chart and sends a
569     * change event to all registered listeners.
570     * 
571     * @param offset  the new offset ({@code null} not permitted).
572     */
573    @Override
574    public void setTranslate2D(Offset2D offset) {
575        ArgChecks.nullNotPermitted(offset, "offset");
576        this.translate2D = offset;
577        fireChangeEvent();
578    }
579    
580    /**
581     * Returns the legend builder.  The default value is an instance of
582     * {@link StandardLegendBuilder}.  If the legend builder is {@code null}, 
583     * no legend will be displayed for the chart.
584     * 
585     * @return The legend builder (possibly {@code null}).
586     * 
587     * @see #setLegendBuilder(com.orsoncharts.legend.LegendBuilder) 
588     * @see #setLegendAnchor(com.orsoncharts.util.Anchor2D) 
589     */
590    public LegendBuilder getLegendBuilder() {
591        return this.legendBuilder;
592    }
593    
594    /**
595     * Sets the legend builder and sends a change event to all registered 
596     * listeners.  When the legend builder is {@code null}, no legend 
597     * will be displayed on the chart.
598     * 
599     * @param legendBuilder  the legend builder ({@code null} permitted).
600     * 
601     * @see #setLegendAnchor(com.orsoncharts.util.Anchor2D) 
602     */
603    public void setLegendBuilder(LegendBuilder legendBuilder) {
604        this.legendBuilder = legendBuilder;
605        fireChangeEvent();
606    }
607    
608    /**
609     * Returns the legend anchor.
610     * 
611     * @return The legend anchor (never {@code null}).
612     * 
613     * @see #setLegendAnchor(com.orsoncharts.util.Anchor2D) 
614     */
615    public Anchor2D getLegendAnchor() {
616        return this.legendAnchor;
617    }
618    
619    /**
620     * Sets the legend anchor and sends a {@link Chart3DChangeEvent} to all
621     * registered listeners.  There is a {@link LegendAnchor} class providing
622     * some useful default anchors.
623     * 
624     * @param anchor  the anchor ({@code null} not permitted).
625     * 
626     * @see #getLegendAnchor() 
627     */
628    public void setLegendAnchor(Anchor2D anchor) {
629        ArgChecks.nullNotPermitted(anchor, "anchor");
630        this.legendAnchor = anchor;
631        fireChangeEvent();
632    }
633    
634    /**
635     * Returns the orientation for the legend.
636     * 
637     * @return The orientation (never {@code null}). 
638     * 
639     * @since 1.1
640     */
641    public Orientation getLegendOrientation() {
642        return this.legendOrientation;
643    }
644
645    /**
646     * Sets the legend orientation and sends a {@link Chart3DChangeEvent}
647     * to all registered listeners.
648     * 
649     * @param orientation  the orientation ({@code null} not permitted).
650     * 
651     * @since 1.1
652     */
653    public void setLegendOrientation(Orientation orientation) {
654        ArgChecks.nullNotPermitted(orientation, "orientation");
655        this.legendOrientation = orientation;
656        fireChangeEvent();
657    }
658    
659    /**
660     * Sets the legend position (both the anchor point and the orientation) and
661     * sends a {@link Chart3DChangeEvent} to all registered listeners. 
662     * This is a convenience method that calls both the 
663     * {@link #setLegendAnchor(com.orsoncharts.util.Anchor2D)} and 
664     * {@link #setLegendOrientation(com.orsoncharts.util.Orientation)}
665     * methods.
666     * 
667     * @param anchor  the anchor ({@code null} not permitted).
668     * @param orientation  the orientation ({@code null} not permitted).
669     * 
670     * @since 1.1
671     */
672    public void setLegendPosition(Anchor2D anchor, Orientation orientation) {
673        setNotify(false);
674        setLegendAnchor(anchor);
675        setLegendOrientation(orientation);
676        setNotify(true);
677    }
678    
679    /**
680     * Returns the collection of rendering hints for the chart.
681     *
682     * @return The rendering hints for the chart (never {@code null}).
683     *
684     * @see #setRenderingHints(RenderingHints)
685     * 
686     * @since 1.1
687     */
688    public RenderingHints getRenderingHints() {
689        return this.renderingHints;
690    }
691
692    /**
693     * Sets the rendering hints for the chart.  These will be added (using the
694     * {@code Graphics2D.addRenderingHints()} method) near the start of 
695     * the chart rendering.  Note that calling this method will replace all
696     * existing hints assigned to the chart.  If you simply wish to add an
697     * additional hint, you can use {@code getRenderingHints().put(key, value)}.
698     *
699     * @param hints  the rendering hints ({@code null} not permitted).
700     *
701     * @see #getRenderingHints()
702     * 
703     * @since 1.1
704     */
705    public void setRenderingHints(RenderingHints hints) {
706        ArgChecks.nullNotPermitted(hints, "hints");
707        this.renderingHints = hints;
708        fireChangeEvent();
709    }
710
711    /**
712     * Returns a flag that indicates whether or not anti-aliasing is used when
713     * the chart is drawn.
714     *
715     * @return The flag.
716     *
717     * @see #setAntiAlias(boolean)
718     * @since 1.1
719     */
720    public boolean getAntiAlias() {
721        Object val = this.renderingHints.get(RenderingHints.KEY_ANTIALIASING);
722        return RenderingHints.VALUE_ANTIALIAS_ON.equals(val);
723    }
724
725    /**
726     * Sets a flag that indicates whether or not anti-aliasing is used when the
727     * chart is drawn.
728     * <P>
729     * Anti-aliasing usually improves the appearance of charts, but is slower.
730     *
731     * @param flag  the new value of the flag.
732     *
733     * @see #getAntiAlias()
734     * @since 1.1
735     */
736    public void setAntiAlias(boolean flag) {
737        if (flag) {
738            this.renderingHints.put(RenderingHints.KEY_ANTIALIASING,
739                    RenderingHints.VALUE_ANTIALIAS_ON);
740        } else {
741            this.renderingHints.put(RenderingHints.KEY_ANTIALIASING,
742                    RenderingHints.VALUE_ANTIALIAS_OFF);
743        }
744        fireChangeEvent();
745    }
746 
747    /**
748     * Returns the flag that controls whether or not element hints will be
749     * added to the {@code Graphics2D} output when the chart is rendered.
750     * The default value is {@code false}.
751     * 
752     * @return A boolean.
753     * 
754     * @since 1.3
755     */
756    public boolean getElementHinting() {
757        return this.elementHinting;
758    }
759    
760    /**
761     * Sets the flag that controls whether or not element hints will be
762     * added to the {@code Graphics2D} output when the chart is rendered
763     * and sends a change event to all registered listeners.
764     * 
765     * @param hinting  the new flag value.
766     * 
767     * @since 1.3
768     */
769    public void setElementHinting(boolean hinting) {
770        this.elementHinting = hinting;
771        fireChangeEvent();
772    }
773    
774    /**
775     * Returns the chart style.
776     * 
777     * @return The chart style (never {@code null}).
778     * 
779     * @since 1.2
780     */
781    public ChartStyle getStyle() {
782        return this.style;
783    }
784    
785    /**
786     * Sets (and applies) the specified chart style.
787     * 
788     * @param style  the chart style ({@code null} not permitted).
789     * 
790     * @since 1.2
791     */
792    public void setStyle(ChartStyle style) {
793        ArgChecks.nullNotPermitted(style, "style");
794        this.style.removeChangeListener(this);
795        this.style = style;
796        this.style.addChangeListener(this);
797        setNotify(false);
798        receive(new ChartStyler(this.style));
799        setNotify(true);
800    }
801
802    /**
803     * Creates a world containing the chart and the supplied chart box.
804     * 
805     * @param chartBox  the chart box ({@code null} permitted).
806     */
807    private World createWorld(ChartBox3D chartBox) {
808        World result = new World();      
809        Dimension3D dim = this.plot.getDimensions();
810        double w = dim.getWidth();
811        double h = dim.getHeight();
812        double d = dim.getDepth();
813        if (chartBox != null) {
814            result.add("chartbox", chartBox.createObject3D());
815        }
816        this.plot.compose(result, -w / 2, -h / 2, -d / 2);
817        return result;
818    }
819    
820    /**
821     * Draws the chart to the specified output target.
822     * 
823     * @param g2  the output target ({@code null} not permitted).
824     * 
825     * @return Information about the items rendered.
826     */
827    @Override
828    public RenderingInfo draw(Graphics2D g2, Rectangle2D bounds) {
829        beginElement(g2, this.id, "ORSON_CHART_TOP_LEVEL");
830        Shape savedClip = g2.getClip();
831        g2.clip(bounds);
832        g2.addRenderingHints(this.renderingHints);
833        g2.setStroke(new BasicStroke(1.5f, BasicStroke.CAP_ROUND, 
834                BasicStroke.JOIN_ROUND, 1f));
835        Dimension3D dim3D = this.plot.getDimensions();
836        double w = dim3D.getWidth();
837        double h = dim3D.getHeight();
838        double depth = dim3D.getDepth();
839        ChartBox3D chartBox = null;
840        if (this.plot instanceof XYZPlot 
841                || this.plot instanceof CategoryPlot3D) {
842            double[] tickUnits = findAxisTickUnits(g2, w, h, depth);
843            chartBox = new ChartBox3D(w, h, depth, -w / 2, -h / 2, -depth / 2, 
844                    this.chartBoxColor);
845            chartBox.setXTicks(fetchXTickData(this.plot, tickUnits[0]));
846            chartBox.setYTicks(fetchYTickData(this.plot, tickUnits[1]));
847            chartBox.setZTicks(fetchZTickData(this.plot, tickUnits[2]));
848            chartBox.setXMarkers(fetchXMarkerData(this.plot));
849            chartBox.setYMarkers(fetchYMarkerData(this.plot));
850            chartBox.setZMarkers(fetchZMarkerData(this.plot));
851        }
852        if (this.world == null) {
853            this.world = createWorld(chartBox);
854        } else if (chartBox != null) {
855            this.world.clear("chartbox");
856            this.world.add("chartbox", chartBox.createObject3D());
857        }
858        if (this.background != null) {
859            this.background.fill(g2, bounds);
860        }
861        AffineTransform saved = g2.getTransform();
862        double dx = bounds.getX() + bounds.getWidth() / 2.0 
863                + this.translate2D.getDX();
864        double dy = bounds.getY() + bounds.getHeight() / 2.0 
865                + this.translate2D.getDY();
866        g2.translate(dx, dy);
867        Point3D[] eyePts = this.world.calculateEyeCoordinates(this.viewPoint);
868        Point2D[] pts = this.world.calculateProjectedPoints(this.viewPoint, 
869                this.projDist);
870        
871        // sort faces by z-order
872        List<Face> facesInPaintOrder = new ArrayList<Face>(world.getFaces());
873        facesInPaintOrder = this.faceSorter.sort(facesInPaintOrder, eyePts);
874        Line2D line = null;
875        Stroke stroke = new BasicStroke(1.0f);
876        for (Face f : facesInPaintOrder) {
877            // check for the special case where the face is just a line
878            if (f.getVertexCount() == 2) {
879                g2.setPaint(f.getColor());
880                if (line == null) {
881                    line = new Line2D.Float();
882                }
883                int v0 = f.getVertexIndex(0);
884                int v1 = f.getVertexIndex(1);
885                line.setLine(pts[v0].getX(), pts[v0].getY(), pts[v1].getX(), 
886                        pts[v1].getY());
887                g2.setStroke(stroke);
888                g2.draw(line);
889                continue;
890            }
891            boolean drawOutline = f.getOutline();
892            double[] plane = f.calculateNormal(eyePts);
893            double inprod = plane[0] * world.getSunX() + plane[1]
894                    * world.getSunY() + plane[2] * world.getSunZ();
895            double shade = (inprod + 1) / 2.0;
896            if (f instanceof DoubleSidedFace 
897                    || Utils2D.area2(pts[f.getVertexIndex(0)],
898                    pts[f.getVertexIndex(1)], pts[f.getVertexIndex(2)]) > 0.0) {
899                Color c = f.getColor();
900                Path2D p = f.createPath(pts);
901                g2.setPaint(new Color((int) (c.getRed() * shade),
902                        (int) (c.getGreen() * shade),
903                        (int) (c.getBlue() * shade), c.getAlpha()));
904                if (this.elementHinting) {
905                    beginElementGroup(f, g2);
906                }
907                g2.fill(p);
908                if (drawOutline) {
909                    g2.draw(p);
910                }
911                if (this.elementHinting) {
912                    endElementGroup(f, g2);
913                }
914                
915                if (f instanceof ChartBoxFace 
916                        && (this.plot instanceof CategoryPlot3D 
917                        || this.plot instanceof XYZPlot)) {
918                    Stroke savedStroke = g2.getStroke();
919                    ChartBoxFace cbf = (ChartBoxFace) f;
920                    drawGridlines(g2, cbf, pts);
921                    drawMarkers(g2, cbf, pts);
922                    g2.setStroke(savedStroke);
923                }
924            } else if (f instanceof LabelFace) {
925                LabelFace lf = (LabelFace) f;
926                Path2D p = lf.createPath(pts);
927                Rectangle2D lb = p.getBounds2D();
928                g2.setFont(lf.getFont());
929                g2.setColor(lf.getBackgroundColor());
930                Rectangle2D bb = TextUtils.calcAlignedStringBounds(
931                        lf.getLabel(), g2, 
932                        (float) lb.getCenterX(), (float) lb.getCenterY(), 
933                        TextAnchor.CENTER);
934                g2.fill(bb);
935                g2.setColor(lf.getTextColor());
936                Rectangle2D r = TextUtils.drawAlignedString(lf.getLabel(), g2, 
937                        (float) lb.getCenterX(), (float) lb.getCenterY(), 
938                        TextAnchor.CENTER);
939                lf.getOwner().setProperty("labelBounds", r);
940            } 
941        }
942        RenderingInfo info = new RenderingInfo(facesInPaintOrder, pts, dx, dy);
943        OnDrawHandler onDrawHandler = new OnDrawHandler(info, 
944                this.elementHinting);
945   
946        // handle labels on pie plots...
947        if (this.plot instanceof PiePlot3D) {
948            drawPieLabels(g2, w, h, depth, info);
949        }
950
951        // handle axis labelling on non-pie plots...
952        if (this.plot instanceof XYZPlot || this.plot instanceof 
953                CategoryPlot3D) {
954            drawAxes(g2, chartBox, pts, info);
955        }    
956
957        g2.setTransform(saved);
958        
959        // generate and draw the legend...
960        if (this.legendBuilder != null) {
961            TableElement legend = this.legendBuilder.createLegend(this.plot,
962                    this.legendAnchor, this.legendOrientation, this.style);
963            if (legend != null) {
964                Dimension2D legendSize = legend.preferredSize(g2, bounds);
965                Rectangle2D legendArea = calculateDrawArea(legendSize, 
966                        this.legendAnchor, bounds);
967                legend.draw(g2, legendArea, onDrawHandler);
968            }
969        }
970
971        // draw the title...
972        if (this.title != null) {
973            Dimension2D titleSize = this.title.preferredSize(g2, bounds);
974            Rectangle2D titleArea = calculateDrawArea(titleSize, 
975                    this.titleAnchor, bounds);
976            this.title.draw(g2, titleArea, onDrawHandler);
977        }
978        g2.setClip(savedClip);
979        endElement(g2);
980        return info;
981    }
982    
983    private void beginElementGroup(Face face, Graphics2D g2) {
984        Object3D owner = face.getOwner();
985        ItemKey itemKey = (ItemKey) owner.getProperty(Object3D.ITEM_KEY);
986        if (itemKey != null) {
987            Map<String, String> m = new HashMap<String, String>();
988            m.put("ref", itemKey.toJSONString());
989            g2.setRenderingHint(Chart3DHints.KEY_BEGIN_ELEMENT, m);
990        }
991    }
992    
993    private void endElementGroup(Face face, Graphics2D g2) {
994        Object3D owner = face.getOwner();
995        ItemKey itemKey = (ItemKey) owner.getProperty(Object3D.ITEM_KEY);
996        if (itemKey != null) {
997            g2.setRenderingHint(Chart3DHints.KEY_END_ELEMENT, Boolean.TRUE);
998        }
999    }
1000    
1001    /**
1002     * An implementation method that fetches x-axis tick data from the plot,
1003     * assuming it is either a {@link CategoryPlot3D} or an {@link XYZPlot}.
1004     * On a category plot, the x-axis is the column axis (and the tickUnit is
1005     * ignored).
1006     * 
1007     * @param plot  the plot.
1008     * @param tickUnit  the tick unit.
1009     * 
1010     * @return A list of tick data instances representing the tick marks and
1011     *     values along the x-axis.
1012     */
1013    private List<TickData> fetchXTickData(Plot3D plot, double tickUnit) {
1014        if (plot instanceof CategoryPlot3D) {
1015            CategoryPlot3D cp = (CategoryPlot3D) plot;
1016            return cp.getColumnAxis().generateTickDataForColumns(
1017                    cp.getDataset());
1018        }
1019        if (plot instanceof XYZPlot) {
1020            XYZPlot xp = (XYZPlot) plot;
1021            return xp.getXAxis().generateTickData(tickUnit);
1022        }
1023        return Collections.emptyList();
1024    }
1025
1026    /**
1027     * An implementation method that fetches y-axis tick data from the plot,
1028     * assuming it is either a {@link CategoryPlot3D} or an {@link XYZPlot}.
1029     * On a category plot, the y-axis is the value axis.
1030     * 
1031     * @param plot  the plot.
1032     * @param tickUnit  the tick unit.
1033     * 
1034     * @return A list of tick data instances representing the tick marks and
1035     *     values along the y-axis.
1036     */
1037    private List<TickData> fetchYTickData(Plot3D plot, double tickUnit) {
1038        if (plot instanceof CategoryPlot3D) {
1039            CategoryPlot3D cp = (CategoryPlot3D) plot;
1040            return cp.getValueAxis().generateTickData(tickUnit);
1041        }
1042        if (plot instanceof XYZPlot) {
1043            XYZPlot xp = (XYZPlot) plot;
1044            return xp.getYAxis().generateTickData(tickUnit);
1045        }
1046        return Collections.emptyList(); 
1047    }
1048
1049    /**
1050     * An implementation method that fetches z-axis tick data from the plot,
1051     * assuming it is either a {@link CategoryPlot3D} or an {@link XYZPlot}.
1052     * On a category plot, the z-axis is the row axis (and the tickUnit is
1053     * ignored).
1054     * 
1055     * @param plot  the plot.
1056     * @param tickUnit  the tick unit.
1057     * 
1058     * @return A list of tick data instances representing the tick marks and
1059     *     values along the y-axis.
1060     */
1061    private List<TickData> fetchZTickData(Plot3D plot, double tickUnit) {
1062        if (plot instanceof CategoryPlot3D) {
1063            CategoryPlot3D cp = (CategoryPlot3D) plot;
1064            return cp.getRowAxis().generateTickDataForRows(cp.getDataset());
1065        }
1066        if (plot instanceof XYZPlot) {
1067            XYZPlot xp = (XYZPlot) plot;
1068            return xp.getZAxis().generateTickData(tickUnit);
1069        }
1070        return Collections.emptyList(); 
1071    }
1072    
1073    /**
1074     * Fetches marker data for the plot's x-axis.
1075     * 
1076     * @param plot  the plot ({@code null} not permitted).
1077     * 
1078     * @return A list of marker data items (possibly empty but never 
1079     *         {@code null}).
1080     */
1081    private List<MarkerData> fetchXMarkerData(Plot3D plot) {
1082        if (plot instanceof CategoryPlot3D) {
1083            return ((CategoryPlot3D) plot).getColumnAxis().generateMarkerData();
1084        }
1085        if (plot instanceof XYZPlot) {
1086             return ((XYZPlot) plot).getXAxis().generateMarkerData();
1087        }
1088        return new ArrayList<MarkerData>(0);    
1089    }
1090    
1091    /**
1092     * Fetches marker data for the plot's x-axis.
1093     * 
1094     * @param plot  the plot ({@code null} not permitted).
1095     * 
1096     * @return A list of marker data items (possibly empty but never 
1097     *         {@code null}).
1098     */
1099    private List<MarkerData> fetchYMarkerData(Plot3D plot) {
1100        if (plot instanceof CategoryPlot3D) {
1101            return ((CategoryPlot3D) plot).getValueAxis().generateMarkerData();
1102        }
1103        if (plot instanceof XYZPlot) {
1104             return ((XYZPlot) plot).getYAxis().generateMarkerData();
1105        }
1106        return new ArrayList<MarkerData>(0);    
1107    }
1108    
1109    /**
1110     * Fetches marker data for the plot's x-axis.
1111     * 
1112     * @param plot  the plot ({@code null} not permitted).
1113     * 
1114     * @return A list of marker data items (possibly empty but never 
1115     *         {@code null}).
1116     */
1117    private List<MarkerData> fetchZMarkerData(Plot3D plot) {
1118        if (plot instanceof CategoryPlot3D) {
1119            return ((CategoryPlot3D) plot).getRowAxis().generateMarkerData();
1120        }
1121        if (plot instanceof XYZPlot) {
1122             return ((XYZPlot) plot).getZAxis().generateMarkerData();
1123        }
1124        return new ArrayList<MarkerData>(0);    
1125    }
1126    
1127    /**
1128     * Draw the gridlines for one chart box face.
1129     * 
1130     * @param g2  the graphics target.
1131     * @param face  the face.
1132     * @param pts  the projection points.
1133     */
1134    private void drawGridlines(Graphics2D g2, ChartBoxFace face, 
1135            Point2D[] pts) {
1136        if (isGridlinesVisibleForX(this.plot)) {
1137            g2.setPaint(fetchGridlinePaintX(this.plot));
1138            g2.setStroke(fetchGridlineStrokeX(this.plot));
1139            List<TickData> xA = face.getXTicksA();
1140            List<TickData> xB = face.getXTicksB();
1141            for (int i = 0; i < xA.size(); i++) {
1142                Line2D line = new Line2D.Double(
1143                        pts[face.getOffset() + xA.get(i).getVertexIndex()], 
1144                        pts[face.getOffset() + xB.get(i).getVertexIndex()]);
1145                g2.draw(line);
1146            }
1147        }
1148                    
1149        if (isGridlinesVisibleForY(this.plot)) {
1150            g2.setPaint(fetchGridlinePaintY(this.plot));
1151            g2.setStroke(fetchGridlineStrokeY(this.plot));
1152            List<TickData> yA = face.getYTicksA();
1153            List<TickData> yB = face.getYTicksB();
1154            for (int i = 0; i < yA.size(); i++) {
1155                Line2D line = new Line2D.Double(
1156                        pts[face.getOffset() + yA.get(i).getVertexIndex()], 
1157                        pts[face.getOffset() + yB.get(i).getVertexIndex()]);
1158                g2.draw(line);
1159            }
1160        }
1161                    
1162        if (isGridlinesVisibleForZ(this.plot)) {
1163            g2.setPaint(fetchGridlinePaintZ(this.plot));
1164            g2.setStroke(fetchGridlineStrokeZ(this.plot));
1165            List<TickData> zA = face.getZTicksA();
1166            List<TickData> zB = face.getZTicksB();
1167            for (int i = 0; i < zA.size(); i++) {
1168                Line2D line = new Line2D.Double(
1169                        pts[face.getOffset() + zA.get(i).getVertexIndex()], 
1170                        pts[face.getOffset() + zB.get(i).getVertexIndex()]);
1171                g2.draw(line);
1172            }
1173        }
1174    }
1175
1176    /**
1177     * Returns {@code true} if gridlines are visible for the x-axis
1178     * (column axis in the case of a {@link CategoryPlot3D}) and 
1179     * {@code false} otherwise.  For pie charts, this method will always
1180     * return {@code false}.
1181     * 
1182     * @param plot  the plot.
1183     * 
1184     * @return A boolean. 
1185     */
1186    private boolean isGridlinesVisibleForX(Plot3D plot) {
1187        if (plot instanceof CategoryPlot3D) {
1188            CategoryPlot3D cp = (CategoryPlot3D) plot;
1189            return cp.getGridlinesVisibleForColumns();
1190        }
1191        if (plot instanceof XYZPlot) {
1192            XYZPlot xp = (XYZPlot) plot;
1193            return xp.isGridlinesVisibleX();
1194        }
1195        return false;
1196    }
1197    
1198    /**
1199     * Returns {@code true} if gridlines are visible for the y-axis
1200     * (value axis in the case of a {@link CategoryPlot3D}) and 
1201     * {@code false} otherwise.
1202     * 
1203     * @param plot  the plot.
1204     * 
1205     * @return A boolean. 
1206     */
1207    private boolean isGridlinesVisibleForY(Plot3D plot) {
1208        if (plot instanceof CategoryPlot3D) {
1209            CategoryPlot3D cp = (CategoryPlot3D) plot;
1210            return cp.getGridlinesVisibleForValues();
1211        }
1212        if (plot instanceof XYZPlot) {
1213            XYZPlot xp = (XYZPlot) plot;
1214            return xp.isGridlinesVisibleY();
1215        }
1216        return false;
1217    }
1218    
1219    /**
1220     * Returns {@code true} if gridlines are visible for the z-axis
1221     * (row axis in the case of a {@link CategoryPlot3D}) and 
1222     * {@code false} otherwise.
1223     * 
1224     * @param plot  the plot.
1225     * 
1226     * @return A boolean. 
1227     */
1228    private boolean isGridlinesVisibleForZ(Plot3D plot) {
1229        if (plot instanceof CategoryPlot3D) {
1230            CategoryPlot3D cp = (CategoryPlot3D) plot;
1231            return cp.getGridlinesVisibleForRows();
1232        }
1233        if (plot instanceof XYZPlot) {
1234            XYZPlot xp = (XYZPlot) plot;
1235            return xp.isGridlinesVisibleZ();
1236        }
1237        return false;
1238    }
1239    
1240    /**
1241     * Returns the paint used to draw gridlines on the x-axis (or column axis
1242     * in the case of {@link CategoryPlot3D}).
1243     * 
1244     * @param plot  the plot.
1245     * 
1246     * @return The paint. 
1247     */
1248    private Paint fetchGridlinePaintX(Plot3D plot) {
1249        if (plot instanceof CategoryPlot3D) {
1250            CategoryPlot3D cp = (CategoryPlot3D) plot;
1251            return cp.getGridlinePaintForColumns();
1252        }
1253        if (plot instanceof XYZPlot) {
1254            XYZPlot xp = (XYZPlot) plot;
1255            return xp.getGridlinePaintX();
1256        }
1257        return null;
1258    }
1259    
1260    /**
1261     * Returns the paint used to draw gridlines on the y-axis (or value axis
1262     * in the case of {@link CategoryPlot3D}).
1263     * 
1264     * @param plot  the plot.
1265     * 
1266     * @return The paint. 
1267     */
1268    private Paint fetchGridlinePaintY(Plot3D plot) {
1269        if (plot instanceof CategoryPlot3D) {
1270            CategoryPlot3D cp = (CategoryPlot3D) plot;
1271            return cp.getGridlinePaintForValues();
1272        }
1273        if (plot instanceof XYZPlot) {
1274            XYZPlot xp = (XYZPlot) plot;
1275            return xp.getGridlinePaintY();
1276        }
1277        return null;
1278    }
1279    
1280    /**
1281     * Returns the paint used to draw gridlines on the z-axis (or row axis
1282     * in the case of {@link CategoryPlot3D}).
1283     * 
1284     * @param plot  the plot.
1285     * 
1286     * @return The paint. 
1287     */
1288    private Paint fetchGridlinePaintZ(Plot3D plot) {
1289        if (plot instanceof CategoryPlot3D) {
1290            CategoryPlot3D cp = (CategoryPlot3D) plot;
1291            return cp.getGridlinePaintForRows();
1292        }
1293        if (plot instanceof XYZPlot) {
1294            XYZPlot xp = (XYZPlot) plot;
1295            return xp.getGridlinePaintZ();
1296        }
1297        return null;
1298    }
1299    
1300    /**
1301     * Returns the stroke used to draw gridlines on the x-axis (or column axis
1302     * in the case of {@link CategoryPlot3D}).
1303     * 
1304     * @param plot  the plot.
1305     * 
1306     * @return The stroke. 
1307     */
1308    private Stroke fetchGridlineStrokeX(Plot3D plot) {
1309        if (plot instanceof CategoryPlot3D) {
1310            CategoryPlot3D cp = (CategoryPlot3D) plot;
1311            return cp.getGridlineStrokeForColumns();
1312        }
1313        if (plot instanceof XYZPlot) {
1314            XYZPlot xp = (XYZPlot) plot;
1315            return xp.getGridlineStrokeX();
1316        }
1317        return null;
1318    }
1319    
1320    /**
1321     * Returns the stroke used to draw gridlines on the y-axis (or value axis
1322     * in the case of {@link CategoryPlot3D}).
1323     * 
1324     * @param plot  the plot.
1325     * 
1326     * @return The stroke. 
1327     */
1328    private Stroke fetchGridlineStrokeY(Plot3D plot) {
1329        if (plot instanceof CategoryPlot3D) {
1330            CategoryPlot3D cp = (CategoryPlot3D) plot;
1331            return cp.getGridlineStrokeForValues();
1332        }
1333        if (plot instanceof XYZPlot) {
1334            XYZPlot xp = (XYZPlot) plot;
1335            return xp.getGridlineStrokeY();
1336        }
1337        return null;
1338    }
1339    
1340    /**
1341     * Returns the stroke used to draw gridlines on the z-axis (or row axis
1342     * in the case of {@link CategoryPlot3D}).
1343     * 
1344     * @param plot  the plot.
1345     * 
1346     * @return The stroke. 
1347     */
1348    private Stroke fetchGridlineStrokeZ(Plot3D plot) {
1349        if (plot instanceof CategoryPlot3D) {
1350            CategoryPlot3D cp = (CategoryPlot3D) plot;
1351            return cp.getGridlineStrokeForRows();
1352        }
1353        if (plot instanceof XYZPlot) {
1354            XYZPlot xp = (XYZPlot) plot;
1355            return xp.getGridlineStrokeZ();
1356        }
1357        return null;
1358    }
1359    
1360    /**
1361     * Draws the pie labels for a {@link PiePlot3D} in 2D-space by creating a 
1362     * temporary world with vertices at anchor points for the labels, then 
1363     * projecting the points to 2D-space.
1364     * 
1365     * @param g2  the graphics target.
1366     * @param w  the width.
1367     * @param h  the height.
1368     * @param depth  the depth.
1369     * @param info  the rendering info ({@code null} permitted).
1370     */
1371    @SuppressWarnings("unchecked")
1372    private void drawPieLabels(Graphics2D g2, double w, double h, 
1373            double depth, RenderingInfo info) {
1374        PiePlot3D p = (PiePlot3D) this.plot;
1375        World labelOverlay = new World();
1376        List<Object3D> objs = p.getLabelFaces(-w / 2, -h / 2, -depth / 2);
1377        for (Object3D obj : objs) {
1378            labelOverlay.add(obj);
1379        }
1380        Point2D[] ppts = labelOverlay.calculateProjectedPoints(
1381                this.viewPoint, this.projDist);
1382        for (int i = 0; i < p.getDataset().getItemCount() * 2; i++) {
1383            if (p.getDataset().getValue(i / 2) == null) {
1384                continue;
1385            }
1386            Face f = labelOverlay.getFaces().get(i);
1387            if (Utils2D.area2(ppts[f.getVertexIndex(0)], 
1388                    ppts[f.getVertexIndex(1)], 
1389                    ppts[f.getVertexIndex(2)]) > 0) {
1390                Comparable<?> key = p.getDataset().getKey(i / 2);
1391                g2.setColor(p.getSectionLabelColorSource().getColor(key));
1392                g2.setFont(p.getSectionLabelFontSource().getFont(key));
1393                Point2D pt = Utils2D.centerPoint(ppts[f.getVertexIndex(0)], 
1394                        ppts[f.getVertexIndex(1)], ppts[f.getVertexIndex(2)],
1395                        ppts[f.getVertexIndex(3)]);
1396                String label = p.getSectionLabelGenerator().generateLabel(
1397                        p.getDataset(), key);
1398                String ref = "{\"type\": \"sectionLabel\", \"key\": \"" 
1399                        + key.toString() + "\"}";
1400                beginElementWithRef(g2, ref);
1401                Rectangle2D bounds = TextUtils.drawAlignedString(label, g2, 
1402                        (float) pt.getX(), (float) pt.getY(), 
1403                        TextAnchor.CENTER);
1404                endElement(g2);
1405                
1406                if (info != null) {
1407                    RenderedElement pieLabelRE = new RenderedElement(
1408                            InteractiveElementType.SECTION_LABEL, bounds);
1409                    pieLabelRE.setProperty("key", key);
1410                    info.addOffsetElement(pieLabelRE);
1411                }
1412            }
1413        }
1414    }
1415    
1416    private void beginElementWithRef(Graphics2D g2, String ref) {
1417        beginElement(g2, null, ref);    
1418    }
1419    
1420    private void beginElement(Graphics2D g2, String id, String ref) {
1421        if (this.elementHinting) {
1422            Map<String, String> m = new HashMap<String, String>();
1423            if (id != null) {
1424                m.put("id", id);
1425            }
1426            m.put("ref", ref);            
1427            g2.setRenderingHint(Chart3DHints.KEY_BEGIN_ELEMENT, m);            
1428        }    
1429    }
1430
1431    private void endElement(Graphics2D g2) {
1432        if (this.elementHinting) {
1433            g2.setRenderingHint(Chart3DHints.KEY_END_ELEMENT, Boolean.TRUE);
1434        }
1435    }
1436    
1437    /**
1438     * Determines appropriate tick units for the axes in the chart.
1439     * 
1440     * @param g2  the graphics target.
1441     * @param w  the width.
1442     * @param h  the height.
1443     * @param depth  the depth.
1444     * 
1445     * @return The tick sizes. 
1446     */
1447    private double[] findAxisTickUnits(Graphics2D g2, double w, double h, 
1448            double depth) {
1449        World tempWorld = new World();
1450        ChartBox3D chartBox = new ChartBox3D(w, h, depth, -w / 2.0, -h / 2.0, 
1451                -depth / 2.0, Color.WHITE);
1452        tempWorld.add(chartBox.createObject3D());
1453        Point2D[] axisPts2D = tempWorld.calculateProjectedPoints(
1454                this.viewPoint, this.projDist);
1455
1456        // vertices
1457        Point2D v0 = axisPts2D[0];
1458        Point2D v1 = axisPts2D[1];
1459        Point2D v2 = axisPts2D[2];
1460        Point2D v3 = axisPts2D[3];
1461        Point2D v4 = axisPts2D[4];
1462        Point2D v5 = axisPts2D[5];
1463        Point2D v6 = axisPts2D[6];
1464        Point2D v7 = axisPts2D[7];
1465
1466        // faces
1467        boolean a = chartBox.faceA().isFrontFacing(axisPts2D);
1468        boolean b = chartBox.faceB().isFrontFacing(axisPts2D);
1469        boolean c = chartBox.faceC().isFrontFacing(axisPts2D);
1470        boolean d = chartBox.faceD().isFrontFacing(axisPts2D);
1471        boolean e = chartBox.faceE().isFrontFacing(axisPts2D);
1472        boolean f = chartBox.faceF().isFrontFacing(axisPts2D);
1473
1474        double xtick = 0, ytick = 0, ztick = 0;
1475        Axis3D xAxis = null;
1476        ValueAxis3D yAxis = null;
1477        Axis3D zAxis = null;
1478        if (this.plot instanceof XYZPlot) {
1479            XYZPlot pp = (XYZPlot) this.plot;
1480            xAxis = pp.getXAxis();
1481            yAxis = pp.getYAxis();
1482            zAxis = pp.getZAxis();
1483        } else if (this.plot instanceof CategoryPlot3D) {
1484            CategoryPlot3D pp = (CategoryPlot3D) this.plot;
1485            xAxis = pp.getColumnAxis();
1486            yAxis = pp.getValueAxis();
1487            zAxis = pp.getRowAxis();
1488        }
1489            
1490        if (xAxis != null && yAxis != null && zAxis != null) {
1491            double ab = (count(a, b) == 1 ? v0.distance(v1) : 0.0);
1492            double bc = (count(b, c) == 1 ? v3.distance(v2) : 0.0);
1493            double cd = (count(c, d) == 1 ? v4.distance(v7) : 0.0);
1494            double da = (count(d, a) == 1 ? v5.distance(v6) : 0.0);
1495            double be = (count(b, e) == 1 ? v0.distance(v3) : 0.0);
1496            double bf = (count(b, f) == 1 ? v1.distance(v2) : 0.0);
1497            double df = (count(d, f) == 1 ? v6.distance(v7) : 0.0);
1498            double de = (count(d, e) == 1 ? v5.distance(v4) : 0.0);
1499            double ae = (count(a, e) == 1 ? v0.distance(v5) : 0.0);
1500            double af = (count(a, f) == 1 ? v1.distance(v6) : 0.0);
1501            double cf = (count(c, f) == 1 ? v2.distance(v7) : 0.0);
1502            double ce = (count(c, e) == 1 ? v3.distance(v4) : 0.0);
1503
1504            if (count(a, b) == 1 && longest(ab, bc, cd, da)) {
1505                if (xAxis instanceof ValueAxis3D) {
1506                    xtick = ((ValueAxis3D) xAxis).selectTick(g2, v0, v1, v7);
1507                }
1508            }
1509            if (count(b, c) == 1 && longest(bc, ab, cd, da)) {
1510                if (xAxis instanceof ValueAxis3D) {
1511                    xtick = ((ValueAxis3D) xAxis).selectTick(g2, v3, v2, v6);
1512                }
1513            }
1514            if (count(c, d) == 1 && longest(cd, ab, bc, da)) {
1515                if (xAxis instanceof ValueAxis3D) {
1516                    xtick = ((ValueAxis3D) xAxis).selectTick(g2, v4, v7, v1);
1517                }
1518            }
1519            if (count(d, a) == 1 && longest(da, ab, bc, cd)) {
1520                if (xAxis instanceof ValueAxis3D) {
1521                    xtick = ((ValueAxis3D) xAxis).selectTick(g2, v5, v6, v3);
1522                }
1523            }
1524
1525            if (count(b, e) == 1 && longest(be, bf, df, de)) {
1526                ytick = yAxis.selectTick(g2, v0, v3, v7);
1527            }
1528            if (count(b, f) == 1 && longest(bf, be, df, de)) {
1529                ytick = yAxis.selectTick(g2, v1, v2, v4);
1530            }
1531            if (count(d, f) == 1 && longest(df, be, bf, de)) {
1532                ytick = yAxis.selectTick(g2, v6, v7, v0);
1533            }
1534            if (count(d, e) == 1 && longest(de, be, bf, df)) {
1535                ytick = yAxis.selectTick(g2, v5, v4, v1);
1536            }
1537
1538            if (count(a, e) == 1 && longest(ae, af, cf, ce)) {
1539                if (zAxis instanceof ValueAxis3D) {
1540                    ztick = ((ValueAxis3D) zAxis).selectTick(g2, v0, v5, v2);
1541                }
1542            }
1543            if (count(a, f) == 1 && longest(af, ae, cf, ce)) {
1544                if (zAxis instanceof ValueAxis3D) {
1545                    ztick = ((ValueAxis3D) zAxis).selectTick(g2, v1, v6, v3);
1546                }
1547            }
1548            if (count(c, f) == 1 && longest(cf, ae, af, ce)) {
1549                if (zAxis instanceof ValueAxis3D) {
1550                    ztick = ((ValueAxis3D) zAxis).selectTick(g2, v2, v7, v5);
1551                }
1552            }
1553            if (count(c, e) == 1 && longest(ce, ae, af, cf)) {
1554                if (zAxis instanceof ValueAxis3D) {
1555                    ztick = ((ValueAxis3D) zAxis).selectTick(g2, v3, v4, v6);
1556                }
1557            }
1558        }
1559        return new double[] { xtick, ytick, ztick };
1560    }
1561    
1562    private void populateAnchorPoints(List<TickData> tickData, Point2D[] pts) {
1563        for (TickData t : tickData) {
1564            t.setAnchorPt(pts[t.getVertexIndex()]);
1565        }    
1566    }
1567    
1568    /**
1569     * Draws the axes for a chart.
1570     * 
1571     * @param g2  the graphics target ({@code null} not permitted).
1572     * @param chartBox  the chart box (this contains projected points for
1573     *     the tick marks and labels)
1574     * @param pts  the projected points.
1575     * @param info  an object to be populated with rendering info, if it is
1576     *     non-{@code null}.
1577     */
1578    private void drawAxes(Graphics2D g2, ChartBox3D chartBox, Point2D[] pts,
1579            RenderingInfo info) {
1580
1581        // vertices
1582        Point2D v0 = pts[0];
1583        Point2D v1 = pts[1];
1584        Point2D v2 = pts[2];
1585        Point2D v3 = pts[3];
1586        Point2D v4 = pts[4];
1587        Point2D v5 = pts[5];
1588        Point2D v6 = pts[6];
1589        Point2D v7 = pts[7];
1590
1591        // faces
1592        boolean a = chartBox.faceA().isFrontFacing(pts);
1593        boolean b = chartBox.faceB().isFrontFacing(pts);
1594        boolean c = chartBox.faceC().isFrontFacing(pts);
1595        boolean d = chartBox.faceD().isFrontFacing(pts);
1596        boolean e = chartBox.faceE().isFrontFacing(pts);
1597        boolean f = chartBox.faceF().isFrontFacing(pts);
1598
1599        Axis3D xAxis = null, yAxis = null, zAxis = null;
1600        if (this.plot instanceof XYZPlot) {
1601            XYZPlot pp = (XYZPlot) this.plot;
1602            xAxis = pp.getXAxis();
1603            yAxis = pp.getYAxis();
1604            zAxis = pp.getZAxis();
1605        } else if (this.plot instanceof CategoryPlot3D) {
1606            CategoryPlot3D pp = (CategoryPlot3D) this.plot;
1607            xAxis = pp.getColumnAxis();
1608            yAxis = pp.getValueAxis();
1609            zAxis = pp.getRowAxis();
1610        }
1611            
1612        if (xAxis != null && yAxis != null && zAxis != null) {
1613            double ab = (count(a, b) == 1 ? v0.distance(v1) : 0.0);
1614            double bc = (count(b, c) == 1 ? v3.distance(v2) : 0.0);
1615            double cd = (count(c, d) == 1 ? v4.distance(v7) : 0.0);
1616            double da = (count(d, a) == 1 ? v5.distance(v6) : 0.0);
1617            double be = (count(b, e) == 1 ? v0.distance(v3) : 0.0);
1618            double bf = (count(b, f) == 1 ? v1.distance(v2) : 0.0);
1619            double df = (count(d, f) == 1 ? v6.distance(v7) : 0.0);
1620            double de = (count(d, e) == 1 ? v5.distance(v4) : 0.0);
1621            double ae = (count(a, e) == 1 ? v0.distance(v5) : 0.0);
1622            double af = (count(a, f) == 1 ? v1.distance(v6) : 0.0);
1623            double cf = (count(c, f) == 1 ? v2.distance(v7) : 0.0);
1624            double ce = (count(c, e) == 1 ? v3.distance(v4) : 0.0);
1625
1626            List<TickData> ticks; 
1627            if (count(a, b) == 1 && longest(ab, bc, cd, da)) {
1628                ticks = chartBox.faceA().getXTicksA();
1629                populateAnchorPoints(ticks, pts);
1630                xAxis.draw(g2, v0, v1, v7, ticks, info, this.elementHinting);
1631            }
1632            if (count(b, c) == 1 && longest(bc, ab, cd, da)) {
1633                ticks = chartBox.faceB().getXTicksB();
1634                populateAnchorPoints(ticks, pts);
1635                xAxis.draw(g2, v3, v2, v6, ticks, info, this.elementHinting);
1636            }
1637            if (count(c, d) == 1 && longest(cd, ab, bc, da)) {
1638                ticks = chartBox.faceC().getXTicksB();
1639                populateAnchorPoints(ticks, pts);
1640                xAxis.draw(g2, v4, v7, v1, ticks, info, this.elementHinting);
1641            }
1642            if (count(d, a) == 1 && longest(da, ab, bc, cd)) {
1643                ticks = chartBox.faceA().getXTicksB();
1644                populateAnchorPoints(ticks, pts);
1645                xAxis.draw(g2, v5, v6, v3, ticks, info, this.elementHinting);
1646            }
1647
1648            if (count(b, e) == 1 && longest(be, bf, df, de)) {
1649                ticks = chartBox.faceB().getYTicksA();
1650                populateAnchorPoints(ticks, pts);
1651                yAxis.draw(g2, v0, v3, v7, ticks, info, this.elementHinting);
1652            }
1653            if (count(b, f) == 1 && longest(bf, be, df, de)) {
1654                ticks = chartBox.faceB().getYTicksB();
1655                populateAnchorPoints(ticks, pts);
1656                yAxis.draw(g2, v1, v2, v4, ticks, info, this.elementHinting);
1657            }
1658            if (count(d, f) == 1 && longest(df, be, bf, de)) {
1659                ticks = chartBox.faceD().getYTicksA();
1660                populateAnchorPoints(ticks, pts);
1661                yAxis.draw(g2, v6, v7, v0, ticks, info, this.elementHinting);
1662            }
1663            if (count(d, e) == 1 && longest(de, be, bf, df)) {
1664                ticks = chartBox.faceD().getYTicksB();
1665                populateAnchorPoints(ticks, pts);
1666                yAxis.draw(g2, v5, v4, v1, ticks, info, this.elementHinting);
1667            }
1668
1669            if (count(a, e) == 1 && longest(ae, af, cf, ce)) {
1670                ticks = chartBox.faceA().getZTicksA();
1671                populateAnchorPoints(ticks, pts);
1672                zAxis.draw(g2, v0, v5, v2, ticks, info, this.elementHinting);
1673            }
1674            if (count(a, f) == 1 && longest(af, ae, cf, ce)) {
1675                ticks = chartBox.faceA().getZTicksB();
1676                populateAnchorPoints(ticks, pts);
1677                zAxis.draw(g2, v1, v6, v3, ticks, info, this.elementHinting);
1678            }
1679            if (count(c, f) == 1 && longest(cf, ae, af, ce)) {
1680                ticks = chartBox.faceC().getZTicksB();
1681                populateAnchorPoints(ticks, pts);
1682                zAxis.draw(g2, v2, v7, v5, ticks, info, this.elementHinting);
1683            }
1684            if (count(c, e) == 1 && longest(ce, ae, af, cf)) {
1685                ticks = chartBox.faceC().getZTicksA();
1686                populateAnchorPoints(ticks, pts);
1687                zAxis.draw(g2, v3, v4, v6, ticks, info, this.elementHinting);
1688            }
1689        }
1690    }
1691
1692    /**
1693     * Draws the markers for one face on a chart box.  The {@code pts}
1694     * array contains all the projected points for all the vertices in the
1695     * world...the chart box face references the required points by index.
1696     * 
1697     * @param g2  the graphics target ({@code null} not permitted).
1698     * @param face  the face of the chart box ({@code null} not permitted).
1699     * @param pts  the projected points for the whole world.
1700     */
1701    private void drawMarkers(Graphics2D g2, ChartBoxFace face, Point2D[] pts) {
1702        // x markers
1703        List<MarkerData> xmarkers = face.getXMarkers();
1704        for (MarkerData m : xmarkers) {
1705            m.updateProjection(pts);
1706            Marker marker = fetchXMarker(this.plot, m.getMarkerKey());
1707            beginElementWithRef(g2, "{\"type\": \"xMarker\", \"key\": \"" 
1708                    + m.getMarkerKey() + "\"}");
1709            marker.draw(g2, m, true);
1710            endElement(g2);
1711        }
1712        
1713        // y markers
1714        List<MarkerData> ymarkers = face.getYMarkers();
1715        for (MarkerData m : ymarkers) {
1716            m.updateProjection(pts);
1717            Marker marker = fetchYMarker(this.plot, m.getMarkerKey());
1718            beginElementWithRef(g2, "{\"type\": \"yMarker\", \"key\": \"" 
1719                    + m.getMarkerKey() + "\"}");
1720            marker.draw(g2, m, false);                
1721            endElement(g2);
1722        }
1723        
1724        // z markers
1725        List<MarkerData> zmarkers = face.getZMarkers();
1726        for (MarkerData m : zmarkers) {
1727            m.updateProjection(pts);
1728            beginElementWithRef(g2, "{\"type\": \"zMarker\", \"key\": \"" 
1729                    + m.getMarkerKey() + "\"}");
1730            Marker marker = fetchZMarker(this.plot, m.getMarkerKey());
1731            marker.draw(g2, m, false);
1732            endElement(g2);
1733        }
1734    }
1735    
1736    /**
1737     * Returns the marker from the plot's x-axis that has the specified key,
1738     * or {@code null} if there is no marker with that key.
1739     * 
1740     * @param plot  the plot ({@code null} not permitted).
1741     * @param key  the marker key ({@code null} not permitted).
1742     * 
1743     * @return The marker (possibly {@code null}). 
1744     */
1745    private Marker fetchXMarker(Plot3D plot, String key) {
1746        if (plot instanceof CategoryPlot3D) {
1747            return ((CategoryPlot3D) plot).getColumnAxis().getMarker(key);
1748        } else if (plot instanceof XYZPlot) {
1749            return ((XYZPlot) plot).getXAxis().getMarker(key);
1750        }
1751        return null;
1752    }
1753    
1754    /**
1755     * Returns the marker from the plot's y-axis that has the specified key,
1756     * or {@code null} if there is no marker with that key.
1757     * 
1758     * @param plot  the plot ({@code null} not permitted).
1759     * @param key  the marker key ({@code null} not permitted).
1760     * 
1761     * @return The marker (possibly {@code null}). 
1762     */
1763    private Marker fetchYMarker(Plot3D plot, String key) {
1764        if (plot instanceof CategoryPlot3D) {
1765            return ((CategoryPlot3D) plot).getValueAxis().getMarker(key);
1766        } else if (plot instanceof XYZPlot) {
1767            return ((XYZPlot) plot).getYAxis().getMarker(key);
1768        }
1769        return null;
1770    }
1771
1772    /**
1773     * Returns the marker from the plot's z-axis that has the specified key,
1774     * or {@code null} if there is no marker with that key.
1775     * 
1776     * @param plot  the plot ({@code null} not permitted).
1777     * @param key  the marker key ({@code null} not permitted).
1778     * 
1779     * @return The marker (possibly {@code null}). 
1780     */
1781    private Marker fetchZMarker(Plot3D plot, String key) {
1782        if (plot instanceof CategoryPlot3D) {
1783            return ((CategoryPlot3D) plot).getRowAxis().getMarker(key);
1784        } else if (plot instanceof XYZPlot) {
1785            return ((XYZPlot) plot).getZAxis().getMarker(key);
1786        }
1787        return null;
1788    }
1789
1790    /**
1791     * Receives a visitor.  The visitor is first directed to the plot, then
1792     * the visit is completed for the chart.
1793     * 
1794     * @param visitor  the visitor.
1795     * 
1796     * @since 1.2
1797     */
1798    @Override
1799    public void receive(ChartElementVisitor visitor) {
1800        this.plot.receive(visitor);
1801        visitor.visit(this);
1802    }
1803
1804    /**
1805     * Tests this chart for equality with an arbitrary object.
1806     * 
1807     * @param obj  the object ({@code null} not permitted).
1808     * 
1809     * @return A boolean. 
1810     */
1811    @Override
1812    public boolean equals(Object obj) {
1813        if (obj == this) {
1814            return true;
1815        }
1816        if (!(obj instanceof Chart3D)) {
1817            return false;
1818        }
1819        Chart3D that = (Chart3D) obj;
1820        if (!ObjectUtils.equals(this.background, that.background)) {
1821            return false;
1822        }
1823        if (!ObjectUtils.equals(this.title, that.title)) {
1824            return false;
1825        }
1826        if (!this.titleAnchor.equals(that.titleAnchor)) {
1827            return false;
1828        }
1829        if (!ObjectUtils.equalsPaint(this.chartBoxColor, that.chartBoxColor)) {
1830            return false;
1831        }
1832        if (!ObjectUtils.equals(this.legendBuilder, that.legendBuilder)) {
1833            return false;
1834        }
1835        if (!this.legendAnchor.equals(that.legendAnchor)) {
1836            return false;
1837        }
1838        if (this.legendOrientation != that.legendOrientation) {
1839            return false;
1840        }
1841        if (!this.renderingHints.equals(that.renderingHints)) {
1842            return false;
1843        }
1844        if (this.projDist != that.projDist) {
1845            return false;
1846        }
1847        return true;
1848    }
1849
1850    /**
1851     * A utility method that calculates a drawing area based on a bounding area
1852     * and an anchor.
1853     * 
1854     * @param dim  the dimensions for the drawing area ({@code null} not 
1855     *     permitted).
1856     * @param anchor  the anchor ({@code null} not permitted).
1857     * @param bounds  the bounds ({@code null} not permitted).
1858     * 
1859     * @return A drawing area. 
1860     */
1861    private Rectangle2D calculateDrawArea(Dimension2D dim, Anchor2D anchor, 
1862            Rectangle2D bounds) {
1863        ArgChecks.nullNotPermitted(dim, "dim");
1864        ArgChecks.nullNotPermitted(anchor, "anchor");
1865        ArgChecks.nullNotPermitted(bounds, "bounds");
1866        double x, y;
1867        double w = Math.min(dim.getWidth(), bounds.getWidth());
1868        double h = Math.min(dim.getHeight(), bounds.getHeight());
1869        if (anchor.getRefPt().equals(RefPt2D.CENTER)) {
1870            x = bounds.getCenterX() - w / 2.0;
1871            y = bounds.getCenterY() - h / 2.0;
1872        } else if (anchor.getRefPt().equals(RefPt2D.CENTER_LEFT)) {
1873            x = bounds.getX() + anchor.getOffset().getDX();
1874            y = bounds.getCenterY() - h / 2.0;
1875        } else if (anchor.getRefPt().equals(RefPt2D.CENTER_RIGHT)) {
1876            x = bounds.getMaxX() - anchor.getOffset().getDX() - dim.getWidth();
1877            y = bounds.getCenterY() - h / 2.0;
1878        } else if (anchor.getRefPt().equals(RefPt2D.TOP_CENTER)) {
1879            x = bounds.getCenterX() - w / 2.0;
1880            y = bounds.getY() + anchor.getOffset().getDY();
1881        } else if (anchor.getRefPt().equals(RefPt2D.TOP_LEFT)) {
1882            x = bounds.getX() + anchor.getOffset().getDX();
1883            y = bounds.getY() + anchor.getOffset().getDY();
1884        } else if (anchor.getRefPt().equals(RefPt2D.TOP_RIGHT)) {
1885            x = bounds.getMaxX() - anchor.getOffset().getDX() - dim.getWidth();
1886            y = bounds.getY() + anchor.getOffset().getDY();
1887        } else if (anchor.getRefPt().equals(RefPt2D.BOTTOM_CENTER)) {
1888            x = bounds.getCenterX() - w / 2.0;
1889            y = bounds.getMaxY() - anchor.getOffset().getDY() - dim.getHeight();
1890        } else if (anchor.getRefPt().equals(RefPt2D.BOTTOM_RIGHT)) {
1891            x = bounds.getMaxX() - anchor.getOffset().getDX() - dim.getWidth();
1892            y = bounds.getMaxY() - anchor.getOffset().getDY() - dim.getHeight();
1893        } else if (anchor.getRefPt().equals(RefPt2D.BOTTOM_LEFT)) {
1894            x = bounds.getX() + anchor.getOffset().getDX();
1895            y = bounds.getMaxY() - anchor.getOffset().getDY() - dim.getHeight();
1896        } else {
1897            x = 0.0;
1898            y = 0.0;
1899        }
1900        return new Rectangle2D.Double(x, y, w, h);
1901    }
1902
1903    /**
1904     * Returns {@code true} if x is the longest of the four lengths,
1905     * and {@code false} otherwise.
1906     * 
1907     * @param x  the x-length.
1908     * @param a  length 1.
1909     * @param b  length 2.
1910     * @param c  length 3.
1911     * 
1912     * @return A boolean. 
1913     */
1914    private boolean longest(double x, double a, double b, double c) {
1915        return x >= a && x >= b && x >= c;
1916    }
1917
1918    /**
1919     * Returns the number (0, 1 or 2) arguments that have the value 
1920     * {@code true}.  We use this to examine the visibility of
1921     * adjacent walls of the chart box...where only one wall is visible, there
1922     * is an opportunity to display the axis along that edge.
1923     * 
1924     * @param a  boolean argument 1.
1925     * @param b  boolean argument 2.
1926     * 
1927     * @return 0, 1, or 2.
1928     */
1929    private int count(boolean a, boolean b) {
1930        int result = 0;
1931        if (a) {
1932            result++;
1933        }
1934        if (b) {
1935            result++;
1936        }
1937        return result;
1938    }
1939    
1940    /**
1941     * Receives notification of a plot change event, refreshes the 3D model 
1942     * (world) and passes the event on, wrapped in a {@link Chart3DChangeEvent},
1943     * to all registered listeners.
1944     * 
1945     * @param event  the plot change event. 
1946     */
1947    @Override
1948    public void plotChanged(Plot3DChangeEvent event) {
1949        if (event.requiresWorldUpdate()) {
1950            this.world = null;
1951        }
1952        notifyListeners(new Chart3DChangeEvent(event, this));
1953    }
1954
1955    @Override
1956    public void styleChanged(ChartStyleChangeEvent event) {
1957        ChartStyler styler = new ChartStyler(event.getChartStyle());
1958        receive(styler);
1959        // create a visitor that will visit all chart components and apply the
1960        // style
1961        notifyListeners(new Chart3DChangeEvent(event, this));
1962    }
1963    
1964    /**
1965     * Registers a listener to receive notification of changes to the chart.
1966     * 
1967     * @param listener  the listener ({@code null} not permitted). 
1968     */
1969    public void addChangeListener(Chart3DChangeListener listener) {
1970        this.listenerList.add(Chart3DChangeListener.class, listener);   
1971    }
1972  
1973    /**
1974     * Deregisters a listener so that it no longer receives notification of
1975     * changes to the chart.
1976     * 
1977     * @param listener  the listener ({@code null} not permitted). 
1978     */
1979    public void removeChangeListener(Chart3DChangeListener listener) {
1980        this.listenerList.remove(Chart3DChangeListener.class, listener);  
1981    }
1982  
1983    /**
1984     * Notifies all registered listeners that the chart has been modified.
1985     *
1986     * @param event  information about the change event.
1987     */
1988    public void notifyListeners(Chart3DChangeEvent event) {
1989        // if the 'notify' flag has been switched to false, we don't notify
1990        // the listeners
1991        if (!this.notify) {
1992            return;
1993        }
1994        Object[] listeners = this.listenerList.getListenerList();
1995        for (int i = listeners.length - 2; i >= 0; i -= 2) {
1996            if (listeners[i] == Chart3DChangeListener.class) { 
1997                ((Chart3DChangeListener) listeners[i + 1]).chartChanged(event);
1998            }
1999        }
2000    }
2001  
2002    /**
2003     * Returns a flag that controls whether or not change events are sent to
2004     * registered listeners.
2005     * 
2006     * @return A boolean.
2007     *
2008     * @see #setNotify(boolean)
2009     */
2010    public boolean isNotify() {
2011        return this.notify;
2012    }
2013
2014    /**
2015     * Sets a flag that controls whether or not listeners receive
2016     * {@link Chart3DChangeEvent} notifications.
2017     *
2018     * @param notify  a boolean.
2019     *
2020     * @see #isNotify()
2021     */
2022    public void setNotify(boolean notify) {
2023        this.notify = notify;
2024        // if the flag is being set to true, there may be queued up changes...
2025        if (notify) {
2026            this.world = null;
2027            fireChangeEvent();
2028        }
2029    }
2030  
2031    /**
2032     * Sends a {@link Chart3DChangeEvent} to all registered listeners.
2033     */
2034    protected void fireChangeEvent() {
2035        notifyListeners(new Chart3DChangeEvent(this, this));
2036    }
2037
2038    /**
2039     * Provides serialization support.
2040     *
2041     * @param stream  the input stream.
2042     *
2043     * @throws IOException  if there is an I/O error.
2044     * @throws ClassNotFoundException  if there is a classpath problem.
2045     */
2046    private void readObject(ObjectInputStream stream)
2047            throws IOException, ClassNotFoundException {
2048        stream.defaultReadObject();
2049        // recreate an empty listener list
2050        this.listenerList = new EventListenerList();
2051        this.plot.addChangeListener(this);
2052        // RenderingHints is not easily serialized, so we just put back the
2053        // defaults...
2054        this.renderingHints = new RenderingHints(
2055                RenderingHints.KEY_ANTIALIASING,
2056                RenderingHints.VALUE_ANTIALIAS_ON);
2057        this.renderingHints.put(RenderingHints.KEY_TEXT_ANTIALIASING,
2058                RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
2059                
2060    }
2061    
2062    /**
2063     * Returns a string representing the {@code element}, primarily for
2064     * debugging purposes.
2065     * 
2066     * @param element  the element ({@code null} not permitted).
2067     * 
2068     * @return A string (never {@code null}).
2069     */
2070    public static String renderedElementToString(RenderedElement element) {
2071        Object type = element.getProperty(RenderedElement.TYPE);
2072        if (InteractiveElementType.SECTION_LABEL.equals(type)) {
2073            StringBuilder sb = new StringBuilder();
2074            sb.append("Section label with key '");
2075            Object key = element.getProperty("key");
2076            sb.append(key.toString());
2077            sb.append("'");
2078            return sb.toString();
2079        } else if (InteractiveElementType.LEGEND_ITEM.equals(type)) {
2080            StringBuilder sb = new StringBuilder();
2081            sb.append("Legend item with section key '");
2082            Object key = element.getProperty(Chart3D.SERIES_KEY);
2083            sb.append(key);
2084            sb.append("'");
2085            return sb.toString();
2086        } else if (InteractiveElementType.AXIS_LABEL.equals(type)) {
2087            StringBuilder sb = new StringBuilder();
2088            sb.append("Axis label with the label '");
2089            sb.append(element.getProperty("label"));
2090            sb.append("'");
2091            return sb.toString();
2092        } else if (InteractiveElementType.CATEGORY_AXIS_TICK_LABEL.equals(type)) {
2093            StringBuilder sb = new StringBuilder();
2094            sb.append("Axis tick label with the label '");
2095            sb.append(element.getProperty("label"));
2096            sb.append("'");
2097            return sb.toString();
2098        } else if (InteractiveElementType.VALUE_AXIS_TICK_LABEL.equals(type)) {
2099            StringBuilder sb = new StringBuilder();
2100            sb.append("Axis tick label with the value '");
2101            sb.append(element.getProperty("value"));
2102            sb.append("'");
2103            return sb.toString();
2104        } else if ("obj3d".equals(type)) {
2105            StringBuilder sb = new StringBuilder();
2106            sb.append("An object in the 3D model");
2107            ItemKey itemKey = (ItemKey) element.getProperty(Object3D.ITEM_KEY);
2108            if (itemKey != null) {
2109                sb.append(" representing the data item [");
2110                sb.append(itemKey.toString());
2111                sb.append("]");
2112            }
2113            return sb.toString();
2114        } else {
2115            return element.toString();
2116        }
2117    }
2118
2119}