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 m = new HashMap();
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    private void drawPieLabels(Graphics2D g2, double w, double h, 
1372            double depth, RenderingInfo info) {
1373        PiePlot3D p = (PiePlot3D) this.plot;
1374        World labelOverlay = new World();
1375        List<Object3D> objs = p.getLabelFaces(-w / 2, -h / 2, -depth / 2);
1376        for (Object3D obj : objs) {
1377            labelOverlay.add(obj);
1378        }
1379        Point2D[] ppts = labelOverlay.calculateProjectedPoints(
1380                this.viewPoint, this.projDist);
1381        for (int i = 0; i < p.getDataset().getItemCount() * 2; i++) {
1382            if (p.getDataset().getValue(i / 2) == null) {
1383                continue;
1384            }
1385            Face f = labelOverlay.getFaces().get(i);
1386            if (Utils2D.area2(ppts[f.getVertexIndex(0)], 
1387                    ppts[f.getVertexIndex(1)], 
1388                    ppts[f.getVertexIndex(2)]) > 0) {
1389                Comparable<?> key = p.getDataset().getKey(i / 2);
1390                g2.setColor(p.getSectionLabelColorSource().getColor(key));
1391                g2.setFont(p.getSectionLabelFontSource().getFont(key));
1392                Point2D pt = Utils2D.centerPoint(ppts[f.getVertexIndex(0)], 
1393                        ppts[f.getVertexIndex(1)], ppts[f.getVertexIndex(2)],
1394                        ppts[f.getVertexIndex(3)]);
1395                String label = p.getSectionLabelGenerator().generateLabel(
1396                        p.getDataset(), key);
1397                String ref = "{\"type\": \"sectionLabel\", \"key\": \"" 
1398                        + key.toString() + "\"}";
1399                beginElementWithRef(g2, ref);
1400                Rectangle2D bounds = TextUtils.drawAlignedString(label, g2, 
1401                        (float) pt.getX(), (float) pt.getY(), 
1402                        TextAnchor.CENTER);
1403                endElement(g2);
1404                
1405                if (info != null) {
1406                    RenderedElement pieLabelRE = new RenderedElement(
1407                            InteractiveElementType.SECTION_LABEL, bounds);
1408                    pieLabelRE.setProperty("key", key);
1409                    info.addOffsetElement(pieLabelRE);
1410                }
1411            }
1412        }
1413    }
1414    
1415    private void beginElementWithRef(Graphics2D g2, String ref) {
1416        beginElement(g2, null, ref);    
1417    }
1418    
1419    private void beginElement(Graphics2D g2, String id, String ref) {
1420        if (this.elementHinting) {
1421            Map m = new HashMap<String, String>();
1422            if (id != null) {
1423                m.put("id", id);
1424            }
1425            m.put("ref", ref);            
1426            g2.setRenderingHint(Chart3DHints.KEY_BEGIN_ELEMENT, m);            
1427        }    
1428    }
1429
1430    private void endElement(Graphics2D g2) {
1431        if (this.elementHinting) {
1432            g2.setRenderingHint(Chart3DHints.KEY_END_ELEMENT, Boolean.TRUE);
1433        }
1434    }
1435    
1436    /**
1437     * Determines appropriate tick units for the axes in the chart.
1438     * 
1439     * @param g2  the graphics target.
1440     * @param w  the width.
1441     * @param h  the height.
1442     * @param depth  the depth.
1443     * 
1444     * @return The tick sizes. 
1445     */
1446    private double[] findAxisTickUnits(Graphics2D g2, double w, double h, 
1447            double depth) {
1448        World tempWorld = new World();
1449        ChartBox3D chartBox = new ChartBox3D(w, h, depth, -w / 2.0, -h / 2.0, 
1450                -depth / 2.0, Color.WHITE);
1451        tempWorld.add(chartBox.createObject3D());
1452        Point2D[] axisPts2D = tempWorld.calculateProjectedPoints(
1453                this.viewPoint, this.projDist);
1454
1455        // vertices
1456        Point2D v0 = axisPts2D[0];
1457        Point2D v1 = axisPts2D[1];
1458        Point2D v2 = axisPts2D[2];
1459        Point2D v3 = axisPts2D[3];
1460        Point2D v4 = axisPts2D[4];
1461        Point2D v5 = axisPts2D[5];
1462        Point2D v6 = axisPts2D[6];
1463        Point2D v7 = axisPts2D[7];
1464
1465        // faces
1466        boolean a = chartBox.faceA().isFrontFacing(axisPts2D);
1467        boolean b = chartBox.faceB().isFrontFacing(axisPts2D);
1468        boolean c = chartBox.faceC().isFrontFacing(axisPts2D);
1469        boolean d = chartBox.faceD().isFrontFacing(axisPts2D);
1470        boolean e = chartBox.faceE().isFrontFacing(axisPts2D);
1471        boolean f = chartBox.faceF().isFrontFacing(axisPts2D);
1472
1473        double xtick = 0, ytick = 0, ztick = 0;
1474        Axis3D xAxis = null;
1475        ValueAxis3D yAxis = null;
1476        Axis3D zAxis = null;
1477        if (this.plot instanceof XYZPlot) {
1478            XYZPlot pp = (XYZPlot) this.plot;
1479            xAxis = pp.getXAxis();
1480            yAxis = pp.getYAxis();
1481            zAxis = pp.getZAxis();
1482        } else if (this.plot instanceof CategoryPlot3D) {
1483            CategoryPlot3D pp = (CategoryPlot3D) this.plot;
1484            xAxis = pp.getColumnAxis();
1485            yAxis = pp.getValueAxis();
1486            zAxis = pp.getRowAxis();
1487        }
1488            
1489        if (xAxis != null && yAxis != null && zAxis != null) {
1490            double ab = (count(a, b) == 1 ? v0.distance(v1) : 0.0);
1491            double bc = (count(b, c) == 1 ? v3.distance(v2) : 0.0);
1492            double cd = (count(c, d) == 1 ? v4.distance(v7) : 0.0);
1493            double da = (count(d, a) == 1 ? v5.distance(v6) : 0.0);
1494            double be = (count(b, e) == 1 ? v0.distance(v3) : 0.0);
1495            double bf = (count(b, f) == 1 ? v1.distance(v2) : 0.0);
1496            double df = (count(d, f) == 1 ? v6.distance(v7) : 0.0);
1497            double de = (count(d, e) == 1 ? v5.distance(v4) : 0.0);
1498            double ae = (count(a, e) == 1 ? v0.distance(v5) : 0.0);
1499            double af = (count(a, f) == 1 ? v1.distance(v6) : 0.0);
1500            double cf = (count(c, f) == 1 ? v2.distance(v7) : 0.0);
1501            double ce = (count(c, e) == 1 ? v3.distance(v4) : 0.0);
1502
1503            if (count(a, b) == 1 && longest(ab, bc, cd, da)) {
1504                if (xAxis instanceof ValueAxis3D) {
1505                    xtick = ((ValueAxis3D) xAxis).selectTick(g2, v0, v1, v7);
1506                }
1507            }
1508            if (count(b, c) == 1 && longest(bc, ab, cd, da)) {
1509                if (xAxis instanceof ValueAxis3D) {
1510                    xtick = ((ValueAxis3D) xAxis).selectTick(g2, v3, v2, v6);
1511                }
1512            }
1513            if (count(c, d) == 1 && longest(cd, ab, bc, da)) {
1514                if (xAxis instanceof ValueAxis3D) {
1515                    xtick = ((ValueAxis3D) xAxis).selectTick(g2, v4, v7, v1);
1516                }
1517            }
1518            if (count(d, a) == 1 && longest(da, ab, bc, cd)) {
1519                if (xAxis instanceof ValueAxis3D) {
1520                    xtick = ((ValueAxis3D) xAxis).selectTick(g2, v5, v6, v3);
1521                }
1522            }
1523
1524            if (count(b, e) == 1 && longest(be, bf, df, de)) {
1525                ytick = yAxis.selectTick(g2, v0, v3, v7);
1526            }
1527            if (count(b, f) == 1 && longest(bf, be, df, de)) {
1528                ytick = yAxis.selectTick(g2, v1, v2, v4);
1529            }
1530            if (count(d, f) == 1 && longest(df, be, bf, de)) {
1531                ytick = yAxis.selectTick(g2, v6, v7, v0);
1532            }
1533            if (count(d, e) == 1 && longest(de, be, bf, df)) {
1534                ytick = yAxis.selectTick(g2, v5, v4, v1);
1535            }
1536
1537            if (count(a, e) == 1 && longest(ae, af, cf, ce)) {
1538                if (zAxis instanceof ValueAxis3D) {
1539                    ztick = ((ValueAxis3D) zAxis).selectTick(g2, v0, v5, v2);
1540                }
1541            }
1542            if (count(a, f) == 1 && longest(af, ae, cf, ce)) {
1543                if (zAxis instanceof ValueAxis3D) {
1544                    ztick = ((ValueAxis3D) zAxis).selectTick(g2, v1, v6, v3);
1545                }
1546            }
1547            if (count(c, f) == 1 && longest(cf, ae, af, ce)) {
1548                if (zAxis instanceof ValueAxis3D) {
1549                    ztick = ((ValueAxis3D) zAxis).selectTick(g2, v2, v7, v5);
1550                }
1551            }
1552            if (count(c, e) == 1 && longest(ce, ae, af, cf)) {
1553                if (zAxis instanceof ValueAxis3D) {
1554                    ztick = ((ValueAxis3D) zAxis).selectTick(g2, v3, v4, v6);
1555                }
1556            }
1557        }
1558        return new double[] { xtick, ytick, ztick };
1559    }
1560    
1561    private void populateAnchorPoints(List<TickData> tickData, Point2D[] pts) {
1562        for (TickData t : tickData) {
1563            t.setAnchorPt(pts[t.getVertexIndex()]);
1564        }    
1565    }
1566    
1567    /**
1568     * Draws the axes for a chart.
1569     * 
1570     * @param g2  the graphics target ({@code null} not permitted).
1571     * @param chartBox  the chart box (this contains projected points for
1572     *     the tick marks and labels)
1573     * @param pts  the projected points.
1574     * @param info  an object to be populated with rendering info, if it is
1575     *     non-{@code null}.
1576     */
1577    private void drawAxes(Graphics2D g2, ChartBox3D chartBox, Point2D[] pts,
1578            RenderingInfo info) {
1579
1580        // vertices
1581        Point2D v0 = pts[0];
1582        Point2D v1 = pts[1];
1583        Point2D v2 = pts[2];
1584        Point2D v3 = pts[3];
1585        Point2D v4 = pts[4];
1586        Point2D v5 = pts[5];
1587        Point2D v6 = pts[6];
1588        Point2D v7 = pts[7];
1589
1590        // faces
1591        boolean a = chartBox.faceA().isFrontFacing(pts);
1592        boolean b = chartBox.faceB().isFrontFacing(pts);
1593        boolean c = chartBox.faceC().isFrontFacing(pts);
1594        boolean d = chartBox.faceD().isFrontFacing(pts);
1595        boolean e = chartBox.faceE().isFrontFacing(pts);
1596        boolean f = chartBox.faceF().isFrontFacing(pts);
1597
1598        Axis3D xAxis = null, yAxis = null, zAxis = null;
1599        if (this.plot instanceof XYZPlot) {
1600            XYZPlot pp = (XYZPlot) this.plot;
1601            xAxis = pp.getXAxis();
1602            yAxis = pp.getYAxis();
1603            zAxis = pp.getZAxis();
1604        } else if (this.plot instanceof CategoryPlot3D) {
1605            CategoryPlot3D pp = (CategoryPlot3D) this.plot;
1606            xAxis = pp.getColumnAxis();
1607            yAxis = pp.getValueAxis();
1608            zAxis = pp.getRowAxis();
1609        }
1610            
1611        if (xAxis != null && yAxis != null && zAxis != null) {
1612            double ab = (count(a, b) == 1 ? v0.distance(v1) : 0.0);
1613            double bc = (count(b, c) == 1 ? v3.distance(v2) : 0.0);
1614            double cd = (count(c, d) == 1 ? v4.distance(v7) : 0.0);
1615            double da = (count(d, a) == 1 ? v5.distance(v6) : 0.0);
1616            double be = (count(b, e) == 1 ? v0.distance(v3) : 0.0);
1617            double bf = (count(b, f) == 1 ? v1.distance(v2) : 0.0);
1618            double df = (count(d, f) == 1 ? v6.distance(v7) : 0.0);
1619            double de = (count(d, e) == 1 ? v5.distance(v4) : 0.0);
1620            double ae = (count(a, e) == 1 ? v0.distance(v5) : 0.0);
1621            double af = (count(a, f) == 1 ? v1.distance(v6) : 0.0);
1622            double cf = (count(c, f) == 1 ? v2.distance(v7) : 0.0);
1623            double ce = (count(c, e) == 1 ? v3.distance(v4) : 0.0);
1624
1625            List<TickData> ticks; 
1626            if (count(a, b) == 1 && longest(ab, bc, cd, da)) {
1627                ticks = chartBox.faceA().getXTicksA();
1628                populateAnchorPoints(ticks, pts);
1629                xAxis.draw(g2, v0, v1, v7, ticks, info, this.elementHinting);
1630            }
1631            if (count(b, c) == 1 && longest(bc, ab, cd, da)) {
1632                ticks = chartBox.faceB().getXTicksB();
1633                populateAnchorPoints(ticks, pts);
1634                xAxis.draw(g2, v3, v2, v6, ticks, info, this.elementHinting);
1635            }
1636            if (count(c, d) == 1 && longest(cd, ab, bc, da)) {
1637                ticks = chartBox.faceC().getXTicksB();
1638                populateAnchorPoints(ticks, pts);
1639                xAxis.draw(g2, v4, v7, v1, ticks, info, this.elementHinting);
1640            }
1641            if (count(d, a) == 1 && longest(da, ab, bc, cd)) {
1642                ticks = chartBox.faceA().getXTicksB();
1643                populateAnchorPoints(ticks, pts);
1644                xAxis.draw(g2, v5, v6, v3, ticks, info, this.elementHinting);
1645            }
1646
1647            if (count(b, e) == 1 && longest(be, bf, df, de)) {
1648                ticks = chartBox.faceB().getYTicksA();
1649                populateAnchorPoints(ticks, pts);
1650                yAxis.draw(g2, v0, v3, v7, ticks, info, this.elementHinting);
1651            }
1652            if (count(b, f) == 1 && longest(bf, be, df, de)) {
1653                ticks = chartBox.faceB().getYTicksB();
1654                populateAnchorPoints(ticks, pts);
1655                yAxis.draw(g2, v1, v2, v4, ticks, info, this.elementHinting);
1656            }
1657            if (count(d, f) == 1 && longest(df, be, bf, de)) {
1658                ticks = chartBox.faceD().getYTicksA();
1659                populateAnchorPoints(ticks, pts);
1660                yAxis.draw(g2, v6, v7, v0, ticks, info, this.elementHinting);
1661            }
1662            if (count(d, e) == 1 && longest(de, be, bf, df)) {
1663                ticks = chartBox.faceD().getYTicksB();
1664                populateAnchorPoints(ticks, pts);
1665                yAxis.draw(g2, v5, v4, v1, ticks, info, this.elementHinting);
1666            }
1667
1668            if (count(a, e) == 1 && longest(ae, af, cf, ce)) {
1669                ticks = chartBox.faceA().getZTicksA();
1670                populateAnchorPoints(ticks, pts);
1671                zAxis.draw(g2, v0, v5, v2, ticks, info, this.elementHinting);
1672            }
1673            if (count(a, f) == 1 && longest(af, ae, cf, ce)) {
1674                ticks = chartBox.faceA().getZTicksB();
1675                populateAnchorPoints(ticks, pts);
1676                zAxis.draw(g2, v1, v6, v3, ticks, info, this.elementHinting);
1677            }
1678            if (count(c, f) == 1 && longest(cf, ae, af, ce)) {
1679                ticks = chartBox.faceC().getZTicksB();
1680                populateAnchorPoints(ticks, pts);
1681                zAxis.draw(g2, v2, v7, v5, ticks, info, this.elementHinting);
1682            }
1683            if (count(c, e) == 1 && longest(ce, ae, af, cf)) {
1684                ticks = chartBox.faceC().getZTicksA();
1685                populateAnchorPoints(ticks, pts);
1686                zAxis.draw(g2, v3, v4, v6, ticks, info, this.elementHinting);
1687            }
1688        }
1689    }
1690
1691    /**
1692     * Draws the markers for one face on a chart box.  The {@code pts}
1693     * array contains all the projected points for all the vertices in the
1694     * world...the chart box face references the required points by index.
1695     * 
1696     * @param g2  the graphics target ({@code null} not permitted).
1697     * @param face  the face of the chart box ({@code null} not permitted).
1698     * @param pts  the projected points for the whole world.
1699     */
1700    private void drawMarkers(Graphics2D g2, ChartBoxFace face, Point2D[] pts) {
1701        // x markers
1702        List<MarkerData> xmarkers = face.getXMarkers();
1703        for (MarkerData m : xmarkers) {
1704            m.updateProjection(pts);
1705            Marker marker = fetchXMarker(this.plot, m.getMarkerKey());
1706            beginElementWithRef(g2, "{\"type\": \"xMarker\", \"key\": \"" 
1707                    + m.getMarkerKey() + "\"}");
1708            marker.draw(g2, m, true);
1709            endElement(g2);
1710        }
1711        
1712        // y markers
1713        List<MarkerData> ymarkers = face.getYMarkers();
1714        for (MarkerData m : ymarkers) {
1715            m.updateProjection(pts);
1716            Marker marker = fetchYMarker(this.plot, m.getMarkerKey());
1717            beginElementWithRef(g2, "{\"type\": \"yMarker\", \"key\": \"" 
1718                    + m.getMarkerKey() + "\"}");
1719            marker.draw(g2, m, false);                
1720            endElement(g2);
1721        }
1722        
1723        // z markers
1724        List<MarkerData> zmarkers = face.getZMarkers();
1725        for (MarkerData m : zmarkers) {
1726            m.updateProjection(pts);
1727            beginElementWithRef(g2, "{\"type\": \"zMarker\", \"key\": \"" 
1728                    + m.getMarkerKey() + "\"}");
1729            Marker marker = fetchZMarker(this.plot, m.getMarkerKey());
1730            marker.draw(g2, m, false);
1731            endElement(g2);
1732        }
1733    }
1734    
1735    /**
1736     * Returns the marker from the plot's x-axis that has the specified key,
1737     * or {@code null} if there is no marker with that key.
1738     * 
1739     * @param plot  the plot ({@code null} not permitted).
1740     * @param key  the marker key ({@code null} not permitted).
1741     * 
1742     * @return The marker (possibly {@code null}). 
1743     */
1744    private Marker fetchXMarker(Plot3D plot, String key) {
1745        if (plot instanceof CategoryPlot3D) {
1746            return ((CategoryPlot3D) plot).getColumnAxis().getMarker(key);
1747        } else if (plot instanceof XYZPlot) {
1748            return ((XYZPlot) plot).getXAxis().getMarker(key);
1749        }
1750        return null;
1751    }
1752    
1753    /**
1754     * Returns the marker from the plot's y-axis that has the specified key,
1755     * or {@code null} if there is no marker with that key.
1756     * 
1757     * @param plot  the plot ({@code null} not permitted).
1758     * @param key  the marker key ({@code null} not permitted).
1759     * 
1760     * @return The marker (possibly {@code null}). 
1761     */
1762    private Marker fetchYMarker(Plot3D plot, String key) {
1763        if (plot instanceof CategoryPlot3D) {
1764            return ((CategoryPlot3D) plot).getValueAxis().getMarker(key);
1765        } else if (plot instanceof XYZPlot) {
1766            return ((XYZPlot) plot).getYAxis().getMarker(key);
1767        }
1768        return null;
1769    }
1770
1771    /**
1772     * Returns the marker from the plot's z-axis that has the specified key,
1773     * or {@code null} if there is no marker with that key.
1774     * 
1775     * @param plot  the plot ({@code null} not permitted).
1776     * @param key  the marker key ({@code null} not permitted).
1777     * 
1778     * @return The marker (possibly {@code null}). 
1779     */
1780    private Marker fetchZMarker(Plot3D plot, String key) {
1781        if (plot instanceof CategoryPlot3D) {
1782            return ((CategoryPlot3D) plot).getRowAxis().getMarker(key);
1783        } else if (plot instanceof XYZPlot) {
1784            return ((XYZPlot) plot).getZAxis().getMarker(key);
1785        }
1786        return null;
1787    }
1788
1789    /**
1790     * Receives a visitor.  The visitor is first directed to the plot, then
1791     * the visit is completed for the chart.
1792     * 
1793     * @param visitor  the visitor.
1794     * 
1795     * @since 1.2
1796     */
1797    @Override
1798    public void receive(ChartElementVisitor visitor) {
1799        this.plot.receive(visitor);
1800        visitor.visit(this);
1801    }
1802
1803    /**
1804     * Tests this chart for equality with an arbitrary object.
1805     * 
1806     * @param obj  the object ({@code null} not permitted).
1807     * 
1808     * @return A boolean. 
1809     */
1810    @Override
1811    public boolean equals(Object obj) {
1812        if (obj == this) {
1813            return true;
1814        }
1815        if (!(obj instanceof Chart3D)) {
1816            return false;
1817        }
1818        Chart3D that = (Chart3D) obj;
1819        if (!ObjectUtils.equals(this.background, that.background)) {
1820            return false;
1821        }
1822        if (!ObjectUtils.equals(this.title, that.title)) {
1823            return false;
1824        }
1825        if (!this.titleAnchor.equals(that.titleAnchor)) {
1826            return false;
1827        }
1828        if (!ObjectUtils.equalsPaint(this.chartBoxColor, that.chartBoxColor)) {
1829            return false;
1830        }
1831        if (!ObjectUtils.equals(this.legendBuilder, that.legendBuilder)) {
1832            return false;
1833        }
1834        if (!this.legendAnchor.equals(that.legendAnchor)) {
1835            return false;
1836        }
1837        if (this.legendOrientation != that.legendOrientation) {
1838            return false;
1839        }
1840        if (!this.renderingHints.equals(that.renderingHints)) {
1841            return false;
1842        }
1843        if (this.projDist != that.projDist) {
1844            return false;
1845        }
1846        return true;
1847    }
1848
1849    /**
1850     * A utility method that calculates a drawing area based on a bounding area
1851     * and an anchor.
1852     * 
1853     * @param dim  the dimensions for the drawing area ({@code null} not 
1854     *     permitted).
1855     * @param anchor  the anchor ({@code null} not permitted).
1856     * @param bounds  the bounds ({@code null} not permitted).
1857     * 
1858     * @return A drawing area. 
1859     */
1860    private Rectangle2D calculateDrawArea(Dimension2D dim, Anchor2D anchor, 
1861            Rectangle2D bounds) {
1862        ArgChecks.nullNotPermitted(dim, "dim");
1863        ArgChecks.nullNotPermitted(anchor, "anchor");
1864        ArgChecks.nullNotPermitted(bounds, "bounds");
1865        double x, y;
1866        double w = Math.min(dim.getWidth(), bounds.getWidth());
1867        double h = Math.min(dim.getHeight(), bounds.getHeight());
1868        if (anchor.getRefPt().equals(RefPt2D.CENTER)) {
1869            x = bounds.getCenterX() - w / 2.0;
1870            y = bounds.getCenterY() - h / 2.0;
1871        } else if (anchor.getRefPt().equals(RefPt2D.CENTER_LEFT)) {
1872            x = bounds.getX() + anchor.getOffset().getDX();
1873            y = bounds.getCenterY() - h / 2.0;
1874        } else if (anchor.getRefPt().equals(RefPt2D.CENTER_RIGHT)) {
1875            x = bounds.getMaxX() - anchor.getOffset().getDX() - dim.getWidth();
1876            y = bounds.getCenterY() - h / 2.0;
1877        } else if (anchor.getRefPt().equals(RefPt2D.TOP_CENTER)) {
1878            x = bounds.getCenterX() - w / 2.0;
1879            y = bounds.getY() + anchor.getOffset().getDY();
1880        } else if (anchor.getRefPt().equals(RefPt2D.TOP_LEFT)) {
1881            x = bounds.getX() + anchor.getOffset().getDX();
1882            y = bounds.getY() + anchor.getOffset().getDY();
1883        } else if (anchor.getRefPt().equals(RefPt2D.TOP_RIGHT)) {
1884            x = bounds.getMaxX() - anchor.getOffset().getDX() - dim.getWidth();
1885            y = bounds.getY() + anchor.getOffset().getDY();
1886        } else if (anchor.getRefPt().equals(RefPt2D.BOTTOM_CENTER)) {
1887            x = bounds.getCenterX() - w / 2.0;
1888            y = bounds.getMaxY() - anchor.getOffset().getDY() - dim.getHeight();
1889        } else if (anchor.getRefPt().equals(RefPt2D.BOTTOM_RIGHT)) {
1890            x = bounds.getMaxX() - anchor.getOffset().getDX() - dim.getWidth();
1891            y = bounds.getMaxY() - anchor.getOffset().getDY() - dim.getHeight();
1892        } else if (anchor.getRefPt().equals(RefPt2D.BOTTOM_LEFT)) {
1893            x = bounds.getX() + anchor.getOffset().getDX();
1894            y = bounds.getMaxY() - anchor.getOffset().getDY() - dim.getHeight();
1895        } else {
1896            x = 0.0;
1897            y = 0.0;
1898        }
1899        return new Rectangle2D.Double(x, y, w, h);
1900    }
1901
1902    /**
1903     * Returns {@code true} if x is the longest of the four lengths,
1904     * and {@code false} otherwise.
1905     * 
1906     * @param x  the x-length.
1907     * @param a  length 1.
1908     * @param b  length 2.
1909     * @param c  length 3.
1910     * 
1911     * @return A boolean. 
1912     */
1913    private boolean longest(double x, double a, double b, double c) {
1914        return x >= a && x >= b && x >= c;
1915    }
1916
1917    /**
1918     * Returns the number (0, 1 or 2) arguments that have the value 
1919     * {@code true}.  We use this to examine the visibility of
1920     * adjacent walls of the chart box...where only one wall is visible, there
1921     * is an opportunity to display the axis along that edge.
1922     * 
1923     * @param a  boolean argument 1.
1924     * @param b  boolean argument 2.
1925     * 
1926     * @return 0, 1, or 2.
1927     */
1928    private int count(boolean a, boolean b) {
1929        int result = 0;
1930        if (a) {
1931            result++;
1932        }
1933        if (b) {
1934            result++;
1935        }
1936        return result;
1937    }
1938    
1939    /**
1940     * Receives notification of a plot change event, refreshes the 3D model 
1941     * (world) and passes the event on, wrapped in a {@link Chart3DChangeEvent},
1942     * to all registered listeners.
1943     * 
1944     * @param event  the plot change event. 
1945     */
1946    @Override
1947    public void plotChanged(Plot3DChangeEvent event) {
1948        if (event.requiresWorldUpdate()) {
1949            this.world = null;
1950        }
1951        notifyListeners(new Chart3DChangeEvent(event, this));
1952    }
1953
1954    @Override
1955    public void styleChanged(ChartStyleChangeEvent event) {
1956        ChartStyler styler = new ChartStyler(event.getChartStyle());
1957        receive(styler);
1958        // create a visitor that will visit all chart components and apply the
1959        // style
1960        notifyListeners(new Chart3DChangeEvent(event, this));
1961    }
1962    
1963    /**
1964     * Registers a listener to receive notification of changes to the chart.
1965     * 
1966     * @param listener  the listener ({@code null} not permitted). 
1967     */
1968    public void addChangeListener(Chart3DChangeListener listener) {
1969        this.listenerList.add(Chart3DChangeListener.class, listener);   
1970    }
1971  
1972    /**
1973     * Deregisters a listener so that it no longer receives notification of
1974     * changes to the chart.
1975     * 
1976     * @param listener  the listener ({@code null} not permitted). 
1977     */
1978    public void removeChangeListener(Chart3DChangeListener listener) {
1979        this.listenerList.remove(Chart3DChangeListener.class, listener);  
1980    }
1981  
1982    /**
1983     * Notifies all registered listeners that the chart has been modified.
1984     *
1985     * @param event  information about the change event.
1986     */
1987    public void notifyListeners(Chart3DChangeEvent event) {
1988        // if the 'notify' flag has been switched to false, we don't notify
1989        // the listeners
1990        if (!this.notify) {
1991            return;
1992        }
1993        Object[] listeners = this.listenerList.getListenerList();
1994        for (int i = listeners.length - 2; i >= 0; i -= 2) {
1995            if (listeners[i] == Chart3DChangeListener.class) { 
1996                ((Chart3DChangeListener) listeners[i + 1]).chartChanged(event);
1997            }
1998        }
1999    }
2000  
2001    /**
2002     * Returns a flag that controls whether or not change events are sent to
2003     * registered listeners.
2004     * 
2005     * @return A boolean.
2006     *
2007     * @see #setNotify(boolean)
2008     */
2009    public boolean isNotify() {
2010        return this.notify;
2011    }
2012
2013    /**
2014     * Sets a flag that controls whether or not listeners receive
2015     * {@link Chart3DChangeEvent} notifications.
2016     *
2017     * @param notify  a boolean.
2018     *
2019     * @see #isNotify()
2020     */
2021    public void setNotify(boolean notify) {
2022        this.notify = notify;
2023        // if the flag is being set to true, there may be queued up changes...
2024        if (notify) {
2025            this.world = null;
2026            fireChangeEvent();
2027        }
2028    }
2029  
2030    /**
2031     * Sends a {@link Chart3DChangeEvent} to all registered listeners.
2032     */
2033    protected void fireChangeEvent() {
2034        notifyListeners(new Chart3DChangeEvent(this, this));
2035    }
2036
2037    /**
2038     * Provides serialization support.
2039     *
2040     * @param stream  the input stream.
2041     *
2042     * @throws IOException  if there is an I/O error.
2043     * @throws ClassNotFoundException  if there is a classpath problem.
2044     */
2045    private void readObject(ObjectInputStream stream)
2046            throws IOException, ClassNotFoundException {
2047        stream.defaultReadObject();
2048        // recreate an empty listener list
2049        this.listenerList = new EventListenerList();
2050        this.plot.addChangeListener(this);
2051        // RenderingHints is not easily serialized, so we just put back the
2052        // defaults...
2053        this.renderingHints = new RenderingHints(
2054                RenderingHints.KEY_ANTIALIASING,
2055                RenderingHints.VALUE_ANTIALIAS_ON);
2056        this.renderingHints.put(RenderingHints.KEY_TEXT_ANTIALIASING,
2057                RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
2058                
2059    }
2060    
2061    /**
2062     * Returns a string representing the {@code element}, primarily for
2063     * debugging purposes.
2064     * 
2065     * @param element  the element ({@code null} not permitted).
2066     * 
2067     * @return A string (never {@code null}).
2068     */
2069    public static String renderedElementToString(RenderedElement element) {
2070        Object type = element.getProperty(RenderedElement.TYPE);
2071        if (InteractiveElementType.SECTION_LABEL.equals(type)) {
2072            StringBuilder sb = new StringBuilder();
2073            sb.append("Section label with key '");
2074            Object key = element.getProperty("key");
2075            sb.append(key.toString());
2076            sb.append("'");
2077            return sb.toString();
2078        } else if (InteractiveElementType.LEGEND_ITEM.equals(type)) {
2079            StringBuilder sb = new StringBuilder();
2080            sb.append("Legend item with section key '");
2081            Object key = element.getProperty(Chart3D.SERIES_KEY);
2082            sb.append(key);
2083            sb.append("'");
2084            return sb.toString();
2085        } else if (InteractiveElementType.AXIS_LABEL.equals(type)) {
2086            StringBuilder sb = new StringBuilder();
2087            sb.append("Axis label with the label '");
2088            sb.append(element.getProperty("label"));
2089            sb.append("'");
2090            return sb.toString();
2091        } else if (InteractiveElementType.CATEGORY_AXIS_TICK_LABEL.equals(type)) {
2092            StringBuilder sb = new StringBuilder();
2093            sb.append("Axis tick label with the label '");
2094            sb.append(element.getProperty("label"));
2095            sb.append("'");
2096            return sb.toString();
2097        } else if (InteractiveElementType.VALUE_AXIS_TICK_LABEL.equals(type)) {
2098            StringBuilder sb = new StringBuilder();
2099            sb.append("Axis tick label with the value '");
2100            sb.append(element.getProperty("value"));
2101            sb.append("'");
2102            return sb.toString();
2103        } else if ("obj3d".equals(type)) {
2104            StringBuilder sb = new StringBuilder();
2105            sb.append("An object in the 3D model");
2106            ItemKey itemKey = (ItemKey) element.getProperty(Object3D.ITEM_KEY);
2107            if (itemKey != null) {
2108                sb.append(" representing the data item [");
2109                sb.append(itemKey.toString());
2110                sb.append("]");
2111            }
2112            return sb.toString();
2113        } else {
2114            return element.toString();
2115        }
2116    }
2117
2118}