    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}