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}