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