001/* ===========================================================
002 * JFreeChart : a free chart library for the Java(tm) platform
003 * ===========================================================
004 *
005 * (C) Copyright 2000-2007, by Object Refinery Limited and Contributors.
006 *
007 * Project Info:  http://www.jfree.org/jfreechart/index.html
008 *
009 * This library is free software; you can redistribute it and/or modify it 
010 * under the terms of the GNU Lesser General Public License as published by 
011 * the Free Software Foundation; either version 2.1 of the License, or 
012 * (at your option) any later version.
013 *
014 * This library is distributed in the hope that it will be useful, but 
015 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 
016 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public 
017 * License for more details.
018 *
019 * You should have received a copy of the GNU Lesser General Public
020 * License along with this library; if not, write to the Free Software
021 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, 
022 * USA.  
023 *
024 * [Java is a trademark or registered trademark of Sun Microsystems, Inc. 
025 * in the United States and other countries.]
026 *
027 * --------------
028 * PiePlot3D.java
029 * --------------
030 * (C) Copyright 2000-2007, by Object Refinery and Contributors.
031 *
032 * Original Author:  Tomer Peretz;
033 * Contributor(s):   Richard Atkinson;
034 *                   David Gilbert (for Object Refinery Limited);
035 *                   Xun Kang;
036 *                   Christian W. Zuckschwerdt;
037 *                   Arnaud Lelievre;
038 *                   Dave Crane;
039 *
040 * Changes
041 * -------
042 * 21-Jun-2002 : Version 1;
043 * 31-Jul-2002 : Modified to use startAngle and direction, drawing modified so 
044 *               that charts render with foreground alpha < 1.0 (DG);
045 * 05-Aug-2002 : Small modification to draw method to support URLs for HTML 
046 *               image maps (RA);
047 * 26-Sep-2002 : Fixed errors reported by Checkstyle (DG);
048 * 18-Oct-2002 : Added drawing bug fix sent in by Xun Kang, and made a couple 
049 *               of other related fixes (DG);
050 * 30-Oct-2002 : Changed the PieDataset interface. Fixed another drawing 
051 *               bug (DG);
052 * 12-Nov-2002 : Fixed null pointer exception for zero or negative values (DG);
053 * 07-Mar-2003 : Modified to pass pieIndex on to PieSectionEntity (DG);
054 * 21-Mar-2003 : Added workaround for bug id 620031 (DG);
055 * 26-Mar-2003 : Implemented Serializable (DG);
056 * 30-Jul-2003 : Modified entity constructor (CZ);
057 * 29-Aug-2003 : Small changes for API updates in PiePlot class (DG);
058 * 02-Sep-2003 : Fixed bug where the 'no data' message is not displayed (DG);
059 * 08-Sep-2003 : Added internationalization via use of properties 
060 *               resourceBundle (RFE 690236) (AL); 
061 * 29-Oct-2003 : Added workaround for font alignment in PDF output (DG);
062 * 20-Nov-2003 : Fixed bug 845289 (sides not showing) (DG);
063 * 25-Nov-2003 : Added patch (845095) to fix outline paint issues (DG);
064 * 10-Mar-2004 : Numerous changes to enhance labelling (DG);
065 * 31-Mar-2004 : Adjusted plot area when label generator is null (DG);
066 * 08-Apr-2004 : Added flag to PiePlot class to control the treatment of null 
067 *               values (DG);
068 *               Added pieIndex to PieSectionEntity (DG);
069 * 15-Nov-2004 : Removed creation of default tool tip generator (DG);
070 * 16-Jun-2005 : Added default constructor (DG);
071 * ------------- JFREECHART 1.0.x ---------------------------------------------
072 * 27-Sep-2006 : Updated draw() method for new lookup methods (DG);
073 * 22-Mar-2007 : Added equals() override (DG);
074 * 18-Jun-2007 : Added handling for simple label option (DG);
075 * 04-Oct-2007 : Added option to darken sides of plot - thanks to Alex Moots 
076 *               (see patch 1805262) (DG);
077 *
078 */
079
080package org.jfree.chart.plot;
081
082import java.awt.AlphaComposite;
083import java.awt.Color;
084import java.awt.Composite;
085import java.awt.Font;
086import java.awt.FontMetrics;
087import java.awt.Graphics2D;
088import java.awt.Paint;
089import java.awt.Polygon;
090import java.awt.Shape;
091import java.awt.Stroke;
092import java.awt.geom.Arc2D;
093import java.awt.geom.Area;
094import java.awt.geom.Ellipse2D;
095import java.awt.geom.Point2D;
096import java.awt.geom.Rectangle2D;
097import java.io.Serializable;
098import java.util.ArrayList;
099import java.util.Iterator;
100import java.util.List;
101
102import org.jfree.chart.entity.EntityCollection;
103import org.jfree.chart.entity.PieSectionEntity;
104import org.jfree.chart.event.PlotChangeEvent;
105import org.jfree.chart.labels.PieToolTipGenerator;
106import org.jfree.data.general.DatasetUtilities;
107import org.jfree.data.general.PieDataset;
108import org.jfree.ui.RectangleInsets;
109
110/**
111 * A plot that displays data in the form of a 3D pie chart, using data from
112 * any class that implements the {@link PieDataset} interface.
113 * <P>
114 * Although this class extends {@link PiePlot}, it does not currently support
115 * exploded sections.
116 */
117public class PiePlot3D extends PiePlot implements Serializable {
118
119    /** For serialization. */
120    private static final long serialVersionUID = 3408984188945161432L;
121    
122    /** The factor of the depth of the pie from the plot height */
123    private double depthFactor = 0.2;
124
125    /** 
126     * A flag that controls whether or not the sides of the pie chart
127     * are rendered using a darker colour.
128     * 
129     *  @since 1.0.7.
130     */
131    private boolean darkerSides = false;  // default preserves previous 
132                                          // behaviour
133    
134    /**
135     * Creates a new instance with no dataset.
136     */
137    public PiePlot3D() {
138        this(null);   
139    }
140    
141    /**
142     * Creates a pie chart with a three dimensional effect using the specified 
143     * dataset.
144     *
145     * @param dataset  the dataset (<code>null</code> permitted).
146     */
147    public PiePlot3D(PieDataset dataset) {
148        super(dataset);
149        setCircular(false, false);
150    }
151
152    /**
153     * Returns the depth factor for the chart.
154     *
155     * @return The depth factor.
156     * 
157     * @see #setDepthFactor(double)
158     */
159    public double getDepthFactor() {
160        return this.depthFactor;
161    }
162
163    /**
164     * Sets the pie depth as a percentage of the height of the plot area, and
165     * sends a {@link PlotChangeEvent} to all registered listeners.
166     *
167     * @param factor  the depth factor (for example, 0.20 is twenty percent).
168     * 
169     * @see #getDepthFactor()
170     */
171    public void setDepthFactor(double factor) {
172        this.depthFactor = factor;
173        notifyListeners(new PlotChangeEvent(this));
174    }
175
176    /**
177     * Returns a flag that controls whether or not the sides of the pie chart
178     * are rendered using a darker colour.  This is only applied if the
179     * section colour is an instance of {@link java.awt.Color}.
180     *
181     * @return A boolean.
182     * 
183     * @see #setDarkerSides(boolean)
184     * 
185     * @since 1.0.7
186     */
187    public boolean getDarkerSides() {
188        return this.darkerSides;
189    }
190
191    /**
192     * Sets a flag that controls whether or not the sides of the pie chart
193     * are rendered using a darker colour, and sends a {@link PlotChangeEvent} 
194     * to all registered listeners.  This is only applied if the
195     * section colour is an instance of {@link java.awt.Color}.
196     *
197     * @param darker true to darken the sides, false to use the default 
198     *         behaviour.
199     * 
200     * @see #getDarkerSides()
201     * 
202     * @since 1.0.7.
203     */
204    public void setDarkerSides(boolean darker) {
205        this.darkerSides = darker;
206        notifyListeners(new PlotChangeEvent(this));
207    }
208
209    /**
210     * Draws the plot on a Java 2D graphics device (such as the screen or a 
211     * printer).  This method is called by the 
212     * {@link org.jfree.chart.JFreeChart} class, you don't normally need 
213     * to call it yourself.
214     *
215     * @param g2  the graphics device.
216     * @param plotArea  the area within which the plot should be drawn.
217     * @param anchor  the anchor point.
218     * @param parentState  the state from the parent plot, if there is one.
219     * @param info  collects info about the drawing 
220     *              (<code>null</code> permitted).
221     */
222    public void draw(Graphics2D g2, Rectangle2D plotArea, Point2D anchor,
223                     PlotState parentState,
224                     PlotRenderingInfo info) {
225
226        // adjust for insets...
227        RectangleInsets insets = getInsets();
228        insets.trim(plotArea);
229
230        Rectangle2D originalPlotArea = (Rectangle2D) plotArea.clone();
231        if (info != null) {
232            info.setPlotArea(plotArea);
233            info.setDataArea(plotArea);
234        }
235
236        Shape savedClip = g2.getClip();
237        g2.clip(plotArea);
238
239        // adjust the plot area by the interior spacing value
240        double gapPercent = getInteriorGap();
241        double labelPercent = 0.0;
242        if (getLabelGenerator() != null) {
243            labelPercent = getLabelGap() + getMaximumLabelWidth() 
244                           + getLabelLinkMargin();   
245        }
246        double gapHorizontal = plotArea.getWidth() 
247                               * (gapPercent + labelPercent);
248        double gapVertical = plotArea.getHeight() * gapPercent;
249
250        double linkX = plotArea.getX() + gapHorizontal / 2;
251        double linkY = plotArea.getY() + gapVertical / 2;
252        double linkW = plotArea.getWidth() - gapHorizontal;
253        double linkH = plotArea.getHeight() - gapVertical;
254        
255        // make the link area a square if the pie chart is to be circular...
256        if (isCircular()) { // is circular?
257            double min = Math.min(linkW, linkH) / 2;
258            linkX = (linkX + linkX + linkW) / 2 - min;
259            linkY = (linkY + linkY + linkH) / 2 - min;
260            linkW = 2 * min;
261            linkH = 2 * min;
262        }
263        
264        PiePlotState state = initialise(g2, plotArea, this, null, info);
265        // the explode area defines the max circle/ellipse for the exploded pie 
266        // sections.
267        // it is defined by shrinking the linkArea by the linkMargin factor.
268        double hh = linkW * getLabelLinkMargin();
269        double vv = linkH * getLabelLinkMargin();
270        Rectangle2D explodeArea = new Rectangle2D.Double(linkX + hh / 2.0, 
271                linkY + vv / 2.0, linkW - hh, linkH - vv);
272       
273        state.setExplodedPieArea(explodeArea);
274        
275        // the pie area defines the circle/ellipse for regular pie sections.
276        // it is defined by shrinking the explodeArea by the explodeMargin 
277        // factor. 
278        double maximumExplodePercent = getMaximumExplodePercent();
279        double percent = maximumExplodePercent / (1.0 + maximumExplodePercent);
280        
281        double h1 = explodeArea.getWidth() * percent;
282        double v1 = explodeArea.getHeight() * percent;
283        Rectangle2D pieArea = new Rectangle2D.Double(explodeArea.getX() 
284                + h1 / 2.0, explodeArea.getY() + v1 / 2.0,
285                explodeArea.getWidth() - h1, explodeArea.getHeight() - v1);
286
287        int depth = (int) (pieArea.getHeight() * this.depthFactor);
288        // the link area defines the dog-leg point for the linking lines to 
289        // the labels
290        Rectangle2D linkArea = new Rectangle2D.Double(linkX, linkY, linkW, 
291                linkH - depth);
292        state.setLinkArea(linkArea);   
293
294        state.setPieArea(pieArea);
295        state.setPieCenterX(pieArea.getCenterX());
296        state.setPieCenterY(pieArea.getCenterY() - depth / 2.0);
297        state.setPieWRadius(pieArea.getWidth() / 2.0);
298        state.setPieHRadius((pieArea.getHeight() - depth) / 2.0);
299
300        drawBackground(g2, plotArea);
301        // get the data source - return if null;
302        PieDataset dataset = getDataset();
303        if (DatasetUtilities.isEmptyOrNull(getDataset())) {
304            drawNoDataMessage(g2, plotArea);
305            g2.setClip(savedClip);
306            drawOutline(g2, plotArea);
307            return;
308        }
309
310        // if too any elements
311        if (dataset.getKeys().size() > plotArea.getWidth()) {
312            String text = "Too many elements";
313            Font sfont = new Font("dialog", Font.BOLD, 10);
314            g2.setFont(sfont);
315            FontMetrics fm = g2.getFontMetrics(sfont);
316            int stringWidth = fm.stringWidth(text);
317
318            g2.drawString(text, (int) (plotArea.getX() + (plotArea.getWidth() 
319                    - stringWidth) / 2), (int) (plotArea.getY() 
320                    + (plotArea.getHeight() / 2)));
321            return;
322        }
323        // if we are drawing a perfect circle, we need to readjust the top left
324        // coordinates of the drawing area for the arcs to arrive at this
325        // effect.
326        if (isCircular()) {
327            double min = Math.min(plotArea.getWidth(), 
328                    plotArea.getHeight()) / 2;
329            plotArea = new Rectangle2D.Double(plotArea.getCenterX() - min, 
330                    plotArea.getCenterY() - min, 2 * min, 2 * min);
331        }
332        // get a list of keys...
333        List sectionKeys = dataset.getKeys();
334
335        if (sectionKeys.size() == 0) {
336            return;
337        }
338
339        // establish the coordinates of the top left corner of the drawing area
340        double arcX = pieArea.getX();
341        double arcY = pieArea.getY();
342
343        //g2.clip(clipArea);
344        Composite originalComposite = g2.getComposite();
345        g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 
346                getForegroundAlpha()));
347
348        double totalValue = DatasetUtilities.calculatePieDatasetTotal(dataset);
349        double runningTotal = 0;
350        if (depth < 0) {
351            return;  // if depth is negative don't draw anything
352        }
353
354        ArrayList arcList = new ArrayList();
355        Arc2D.Double arc;
356        Paint paint;
357        Paint outlinePaint;
358        Stroke outlineStroke;
359
360        Iterator iterator = sectionKeys.iterator();
361        while (iterator.hasNext()) {
362
363            Comparable currentKey = (Comparable) iterator.next();
364            Number dataValue = dataset.getValue(currentKey);
365            if (dataValue == null) {
366                arcList.add(null);
367                continue;
368            }
369            double value = dataValue.doubleValue();
370            if (value <= 0) {
371                arcList.add(null);
372                continue;
373            }
374            double startAngle = getStartAngle();
375            double direction = getDirection().getFactor();
376            double angle1 = startAngle + (direction * (runningTotal * 360)) 
377                    / totalValue;
378            double angle2 = startAngle + (direction * (runningTotal + value) 
379                    * 360) / totalValue;
380            if (Math.abs(angle2 - angle1) > getMinimumArcAngleToDraw()) {
381                arcList.add(new Arc2D.Double(arcX, arcY + depth, 
382                        pieArea.getWidth(), pieArea.getHeight() - depth,
383                        angle1, angle2 - angle1, Arc2D.PIE));
384            }
385            else {
386                arcList.add(null);
387            }
388            runningTotal += value;
389        }
390
391        Shape oldClip = g2.getClip();
392
393        Ellipse2D top = new Ellipse2D.Double(pieArea.getX(), pieArea.getY(), 
394                pieArea.getWidth(), pieArea.getHeight() - depth);
395
396        Ellipse2D bottom = new Ellipse2D.Double(pieArea.getX(), pieArea.getY() 
397                + depth, pieArea.getWidth(), pieArea.getHeight() - depth);
398
399        Rectangle2D lower = new Rectangle2D.Double(top.getX(), 
400                top.getCenterY(), pieArea.getWidth(), bottom.getMaxY() 
401                - top.getCenterY());
402
403        Rectangle2D upper = new Rectangle2D.Double(pieArea.getX(), top.getY(), 
404                pieArea.getWidth(), bottom.getCenterY() - top.getY());
405
406        Area a = new Area(top);
407        a.add(new Area(lower));
408        Area b = new Area(bottom);
409        b.add(new Area(upper));
410        Area pie = new Area(a);
411        pie.intersect(b);
412
413        Area front = new Area(pie);
414        front.subtract(new Area(top));
415
416        Area back = new Area(pie);
417        back.subtract(new Area(bottom));
418
419        // draw the bottom circle
420        int[] xs;
421        int[] ys;
422        arc = new Arc2D.Double(arcX, arcY + depth, pieArea.getWidth(), 
423                pieArea.getHeight() - depth, 0, 360, Arc2D.PIE);
424
425        int categoryCount = arcList.size();
426        for (int categoryIndex = 0; categoryIndex < categoryCount; 
427                 categoryIndex++) {
428            arc = (Arc2D.Double) arcList.get(categoryIndex);
429            if (arc == null) {
430                continue;
431            }
432            Comparable key = getSectionKey(categoryIndex);
433            paint = lookupSectionPaint(key, true);
434            outlinePaint = lookupSectionOutlinePaint(key);
435            outlineStroke = lookupSectionOutlineStroke(key);
436            g2.setPaint(paint);
437            g2.fill(arc);
438            g2.setPaint(outlinePaint);
439            g2.setStroke(outlineStroke);
440            g2.draw(arc);
441            g2.setPaint(paint);
442
443            Point2D p1 = arc.getStartPoint();
444
445            // draw the height
446            xs = new int[] {(int) arc.getCenterX(), (int) arc.getCenterX(),
447                    (int) p1.getX(), (int) p1.getX()};
448            ys = new int[] {(int) arc.getCenterY(), (int) arc.getCenterY() 
449                    - depth, (int) p1.getY() - depth, (int) p1.getY()};
450            Polygon polygon = new Polygon(xs, ys, 4);
451            g2.setPaint(java.awt.Color.lightGray);
452            g2.fill(polygon);
453            g2.setPaint(outlinePaint);
454            g2.setStroke(outlineStroke);
455            g2.draw(polygon);
456            g2.setPaint(paint);
457
458        }
459
460        g2.setPaint(Color.gray);
461        g2.fill(back);
462        g2.fill(front);
463
464        // cycle through once drawing only the sides at the back...
465        int cat = 0;
466        iterator = arcList.iterator();
467        while (iterator.hasNext()) {
468            Arc2D segment = (Arc2D) iterator.next();
469            if (segment != null) {
470                Comparable key = getSectionKey(cat);
471                paint = lookupSectionPaint(key, true);
472                outlinePaint = lookupSectionOutlinePaint(key);
473                outlineStroke = lookupSectionOutlineStroke(key);
474                drawSide(g2, pieArea, segment, front, back, paint, 
475                        outlinePaint, outlineStroke, false, true);
476            }
477            cat++;
478        }
479
480        // cycle through again drawing only the sides at the front...
481        cat = 0;
482        iterator = arcList.iterator();
483        while (iterator.hasNext()) {
484            Arc2D segment = (Arc2D) iterator.next();
485            if (segment != null) {
486                Comparable key = getSectionKey(cat);
487                paint = lookupSectionPaint(key);
488                outlinePaint = lookupSectionOutlinePaint(key);
489                outlineStroke = lookupSectionOutlineStroke(key);
490                drawSide(g2, pieArea, segment, front, back, paint, 
491                        outlinePaint, outlineStroke, true, false);
492            }
493            cat++;
494        }
495
496        g2.setClip(oldClip);
497
498        // draw the sections at the top of the pie (and set up tooltips)...
499        Arc2D upperArc;
500        for (int sectionIndex = 0; sectionIndex < categoryCount; 
501                 sectionIndex++) {
502            arc = (Arc2D.Double) arcList.get(sectionIndex);
503            if (arc == null) {
504                continue;
505            }
506            upperArc = new Arc2D.Double(arcX, arcY, pieArea.getWidth(),
507                    pieArea.getHeight() - depth, arc.getAngleStart(), 
508                    arc.getAngleExtent(), Arc2D.PIE);
509            
510            Comparable currentKey = (Comparable) sectionKeys.get(sectionIndex);
511            paint = lookupSectionPaint(currentKey, true);
512            outlinePaint = lookupSectionOutlinePaint(currentKey);
513            outlineStroke = lookupSectionOutlineStroke(currentKey);
514            g2.setPaint(paint);
515            g2.fill(upperArc);
516            g2.setStroke(outlineStroke);
517            g2.setPaint(outlinePaint);
518            g2.draw(upperArc);
519
520           // add a tooltip for the section...
521            if (info != null) {
522                EntityCollection entities 
523                        = info.getOwner().getEntityCollection();
524                if (entities != null) {
525                    String tip = null;
526                    PieToolTipGenerator tipster = getToolTipGenerator();
527                    if (tipster != null) {
528                        // @mgs: using the method's return value was missing 
529                        tip = tipster.generateToolTip(dataset, currentKey);
530                    }
531                    String url = null;
532                    if (getURLGenerator() != null) {
533                        url = getURLGenerator().generateURL(dataset, currentKey,
534                                getPieIndex());
535                    }
536                    PieSectionEntity entity = new PieSectionEntity(
537                            upperArc, dataset, getPieIndex(), sectionIndex, 
538                            currentKey, tip, url);
539                    entities.add(entity);
540                }
541            }
542            List keys = dataset.getKeys();
543            Rectangle2D adjustedPlotArea = new Rectangle2D.Double(
544                    originalPlotArea.getX(), originalPlotArea.getY(), 
545                    originalPlotArea.getWidth(), originalPlotArea.getHeight() 
546                    - depth);
547            if (getSimpleLabels()) {
548                drawSimpleLabels(g2, keys, totalValue, adjustedPlotArea, 
549                        linkArea, state);
550            }
551            else {
552                drawLabels(g2, keys, totalValue, adjustedPlotArea, linkArea, 
553                        state);
554            }
555        }
556
557        g2.setClip(savedClip);
558        g2.setComposite(originalComposite);
559        drawOutline(g2, originalPlotArea);
560
561    }
562
563    /**
564     * Draws the side of a pie section.
565     *
566     * @param g2  the graphics device.
567     * @param plotArea  the plot area.
568     * @param arc  the arc.
569     * @param front  the front of the pie.
570     * @param back  the back of the pie.
571     * @param paint  the color.
572     * @param outlinePaint  the outline paint.
573     * @param outlineStroke  the outline stroke.
574     * @param drawFront  draw the front?
575     * @param drawBack  draw the back?
576     */
577    protected void drawSide(Graphics2D g2,
578                            Rectangle2D plotArea, 
579                            Arc2D arc, 
580                            Area front, 
581                            Area back,
582                            Paint paint, 
583                            Paint outlinePaint,
584                            Stroke outlineStroke,
585                            boolean drawFront, 
586                            boolean drawBack) {
587
588        if (getDarkerSides()) {
589            if (paint instanceof Color) {
590                Color c = (Color) paint;
591                c = c.darker();
592                paint = c;
593            }
594        }
595
596        double start = arc.getAngleStart();
597        double extent = arc.getAngleExtent();
598        double end = start + extent;
599
600        g2.setStroke(outlineStroke);
601        
602        // for CLOCKWISE charts, the extent will be negative...
603        if (extent < 0.0) {
604
605            if (isAngleAtFront(start)) {  // start at front
606
607                if (!isAngleAtBack(end)) {
608
609                    if (extent > -180.0) {  // the segment is entirely at the 
610                                            // front of the chart
611                        if (drawFront) {
612                            Area side = new Area(new Rectangle2D.Double(
613                                    arc.getEndPoint().getX(), plotArea.getY(), 
614                                    arc.getStartPoint().getX() 
615                                    - arc.getEndPoint().getX(),
616                                    plotArea.getHeight()));
617                            side.intersect(front);
618                            g2.setPaint(paint);
619                            g2.fill(side);
620                            g2.setPaint(outlinePaint);
621                            g2.draw(side);
622                        }
623                    }
624                    else {  // the segment starts at the front, and wraps all 
625                            // the way around
626                            // the back and finishes at the front again
627                        Area side1 = new Area(new Rectangle2D.Double(
628                                plotArea.getX(), plotArea.getY(),
629                                arc.getStartPoint().getX() - plotArea.getX(), 
630                                plotArea.getHeight()));
631                        side1.intersect(front);
632
633                        Area side2 = new Area(new Rectangle2D.Double(
634                                arc.getEndPoint().getX(), plotArea.getY(),
635                                plotArea.getMaxX() - arc.getEndPoint().getX(),
636                                plotArea.getHeight()));
637
638                        side2.intersect(front);
639                        g2.setPaint(paint);
640                        if (drawFront) {
641                            g2.fill(side1);
642                            g2.fill(side2);
643                        }
644
645                        if (drawBack) {
646                            g2.fill(back);
647                        }
648
649                        g2.setPaint(outlinePaint);
650                        if (drawFront) {
651                            g2.draw(side1);
652                            g2.draw(side2);
653                        }
654
655                        if (drawBack) {
656                            g2.draw(back);
657                        }
658
659                    }
660                }
661                else {  // starts at the front, finishes at the back (going 
662                        // around the left side)
663
664                    if (drawBack) {
665                        Area side2 = new Area(new Rectangle2D.Double(
666                                plotArea.getX(), plotArea.getY(),
667                                arc.getEndPoint().getX() - plotArea.getX(), 
668                                plotArea.getHeight()));
669                        side2.intersect(back);
670                        g2.setPaint(paint);
671                        g2.fill(side2);
672                        g2.setPaint(outlinePaint);
673                        g2.draw(side2);
674                    }
675
676                    if (drawFront) {
677                        Area side1 = new Area(new Rectangle2D.Double(
678                                plotArea.getX(), plotArea.getY(),
679                                arc.getStartPoint().getX() - plotArea.getX(),
680                                plotArea.getHeight()));
681                        side1.intersect(front);
682                        g2.setPaint(paint);
683                        g2.fill(side1);
684                        g2.setPaint(outlinePaint);
685                        g2.draw(side1);
686                    }
687                }
688            }
689            else {  // the segment starts at the back (still extending 
690                    // CLOCKWISE)
691
692                if (!isAngleAtFront(end)) {
693                    if (extent > -180.0) {  // whole segment stays at the back
694                        if (drawBack) {
695                            Area side = new Area(new Rectangle2D.Double(
696                                    arc.getStartPoint().getX(), plotArea.getY(),
697                                    arc.getEndPoint().getX() 
698                                    - arc.getStartPoint().getX(),
699                                    plotArea.getHeight()));
700                            side.intersect(back);
701                            g2.setPaint(paint);
702                            g2.fill(side);
703                            g2.setPaint(outlinePaint);
704                            g2.draw(side);
705                        }
706                    }
707                    else {  // starts at the back, wraps around front, and 
708                            // finishes at back again
709                        Area side1 = new Area(new Rectangle2D.Double(
710                                arc.getStartPoint().getX(), plotArea.getY(),
711                                plotArea.getMaxX() - arc.getStartPoint().getX(),
712                                plotArea.getHeight()));
713                        side1.intersect(back);
714
715                        Area side2 = new Area(new Rectangle2D.Double(
716                                plotArea.getX(), plotArea.getY(),
717                                arc.getEndPoint().getX() - plotArea.getX(),
718                                plotArea.getHeight()));
719
720                        side2.intersect(back);
721
722                        g2.setPaint(paint);
723                        if (drawBack) {
724                            g2.fill(side1);
725                            g2.fill(side2);
726                        }
727
728                        if (drawFront) {
729                            g2.fill(front);
730                        }
731
732                        g2.setPaint(outlinePaint);
733                        if (drawBack) {
734                            g2.draw(side1);
735                            g2.draw(side2);
736                        }
737
738                        if (drawFront) {
739                            g2.draw(front);
740                        }
741
742                    }
743                }
744                else {  // starts at back, finishes at front (CLOCKWISE)
745
746                    if (drawBack) {
747                        Area side1 = new Area(new Rectangle2D.Double(
748                                arc.getStartPoint().getX(), plotArea.getY(),
749                                plotArea.getMaxX() - arc.getStartPoint().getX(),
750                                plotArea.getHeight()));
751                        side1.intersect(back);
752                        g2.setPaint(paint);
753                        g2.fill(side1);
754                        g2.setPaint(outlinePaint);
755                        g2.draw(side1);
756                    }
757
758                    if (drawFront) {
759                        Area side2 = new Area(new Rectangle2D.Double(
760                                arc.getEndPoint().getX(), plotArea.getY(),
761                                plotArea.getMaxX() - arc.getEndPoint().getX(),
762                                plotArea.getHeight()));
763                        side2.intersect(front);
764                        g2.setPaint(paint);
765                        g2.fill(side2);
766                        g2.setPaint(outlinePaint);
767                        g2.draw(side2);
768                    }
769
770                }
771            }
772        }
773        else if (extent > 0.0) {  // the pie sections are arranged ANTICLOCKWISE
774
775            if (isAngleAtFront(start)) {  // segment starts at the front
776
777                if (!isAngleAtBack(end)) {  // and finishes at the front
778
779                    if (extent < 180.0) {  // segment only occupies the front
780                        if (drawFront) {
781                            Area side = new Area(new Rectangle2D.Double(
782                                    arc.getStartPoint().getX(), plotArea.getY(),
783                                    arc.getEndPoint().getX() 
784                                    - arc.getStartPoint().getX(),
785                                    plotArea.getHeight()));
786                            side.intersect(front);
787                            g2.setPaint(paint);
788                            g2.fill(side);
789                            g2.setPaint(outlinePaint);
790                            g2.draw(side);
791                        }
792                    }
793                    else {  // segments wraps right around the back...
794                        Area side1 = new Area(new Rectangle2D.Double(
795                                arc.getStartPoint().getX(), plotArea.getY(),
796                                plotArea.getMaxX() - arc.getStartPoint().getX(),
797                                plotArea.getHeight()));
798                        side1.intersect(front);
799
800                        Area side2 = new Area(new Rectangle2D.Double(
801                                plotArea.getX(), plotArea.getY(),
802                                arc.getEndPoint().getX() - plotArea.getX(),
803                                plotArea.getHeight()));
804                        side2.intersect(front);
805
806                        g2.setPaint(paint);
807                        if (drawFront) {
808                            g2.fill(side1);
809                            g2.fill(side2);
810                        }
811
812                        if (drawBack) {
813                            g2.fill(back);
814                        }
815
816                        g2.setPaint(outlinePaint);
817                        if (drawFront) {
818                            g2.draw(side1);
819                            g2.draw(side2);
820                        }
821
822                        if (drawBack) {
823                            g2.draw(back);
824                        }
825
826                    }
827                }
828                else {  // segments starts at front and finishes at back...
829                    if (drawBack) {
830                        Area side2 = new Area(new Rectangle2D.Double(
831                                arc.getEndPoint().getX(), plotArea.getY(),
832                                plotArea.getMaxX() - arc.getEndPoint().getX(),
833                                plotArea.getHeight()));
834                        side2.intersect(back);
835                        g2.setPaint(paint);
836                        g2.fill(side2);
837                        g2.setPaint(outlinePaint);
838                        g2.draw(side2);
839                    }
840
841                    if (drawFront) {
842                        Area side1 = new Area(new Rectangle2D.Double(
843                                arc.getStartPoint().getX(), plotArea.getY(),
844                                plotArea.getMaxX() - arc.getStartPoint().getX(),
845                                plotArea.getHeight()));
846                        side1.intersect(front);
847                        g2.setPaint(paint);
848                        g2.fill(side1);
849                        g2.setPaint(outlinePaint);
850                        g2.draw(side1);
851                    }
852                }
853            }
854            else {  // segment starts at back
855
856                if (!isAngleAtFront(end)) {
857                    if (extent < 180.0) {  // and finishes at back
858                        if (drawBack) {
859                            Area side = new Area(new Rectangle2D.Double(
860                                    arc.getEndPoint().getX(), plotArea.getY(),
861                                    arc.getStartPoint().getX() 
862                                    - arc.getEndPoint().getX(),
863                                    plotArea.getHeight()));
864                            side.intersect(back);
865                            g2.setPaint(paint);
866                            g2.fill(side);
867                            g2.setPaint(outlinePaint);
868                            g2.draw(side);
869                        }
870                    }
871                    else {  // starts at back and wraps right around to the 
872                            // back again
873                        Area side1 = new Area(new Rectangle2D.Double(
874                                arc.getStartPoint().getX(), plotArea.getY(),
875                                plotArea.getX() - arc.getStartPoint().getX(),
876                                plotArea.getHeight()));
877                        side1.intersect(back);
878
879                        Area side2 = new Area(new Rectangle2D.Double(
880                                arc.getEndPoint().getX(), plotArea.getY(),
881                                plotArea.getMaxX() - arc.getEndPoint().getX(),
882                                plotArea.getHeight()));
883                        side2.intersect(back);
884
885                        g2.setPaint(paint);
886                        if (drawBack) {
887                            g2.fill(side1);
888                            g2.fill(side2);
889                        }
890
891                        if (drawFront) {
892                            g2.fill(front);
893                        }
894
895                        g2.setPaint(outlinePaint);
896                        if (drawBack) {
897                            g2.draw(side1);
898                            g2.draw(side2);
899                        }
900
901                        if (drawFront) {
902                            g2.draw(front);
903                        }
904
905                    }
906                }
907                else {  // starts at the back and finishes at the front 
908                        // (wrapping the left side)
909                    if (drawBack) {
910                        Area side1 = new Area(new Rectangle2D.Double(
911                                plotArea.getX(), plotArea.getY(),
912                                arc.getStartPoint().getX() - plotArea.getX(),
913                                plotArea.getHeight()));
914                        side1.intersect(back);
915                        g2.setPaint(paint);
916                        g2.fill(side1);
917                        g2.setPaint(outlinePaint);
918                        g2.draw(side1);
919                    }
920
921                    if (drawFront) {
922                        Area side2 = new Area(new Rectangle2D.Double(
923                                plotArea.getX(), plotArea.getY(),
924                                arc.getEndPoint().getX() - plotArea.getX(),
925                                plotArea.getHeight()));
926                        side2.intersect(front);
927                        g2.setPaint(paint);
928                        g2.fill(side2);
929                        g2.setPaint(outlinePaint);
930                        g2.draw(side2);
931                    }
932                }
933            }
934
935        }
936
937    }
938
939    /**
940     * Returns a short string describing the type of plot.
941     *
942     * @return <i>Pie 3D Plot</i>.
943     */
944    public String getPlotType() {
945        return localizationResources.getString("Pie_3D_Plot");
946    }
947
948    /**
949     * A utility method that returns true if the angle represents a point at 
950     * the front of the 3D pie chart.  0 - 180 degrees is the back, 180 - 360 
951     * is the front.
952     *
953     * @param angle  the angle.
954     *
955     * @return A boolean.
956     */
957    private boolean isAngleAtFront(double angle) {
958        return (Math.sin(Math.toRadians(angle)) < 0.0);
959    }
960
961    /**
962     * A utility method that returns true if the angle represents a point at 
963     * the back of the 3D pie chart.  0 - 180 degrees is the back, 180 - 360 
964     * is the front.
965     *
966     * @param angle  the angle.
967     *
968     * @return <code>true</code> if the angle is at the back of the pie.
969     */
970    private boolean isAngleAtBack(double angle) {
971        return (Math.sin(Math.toRadians(angle)) > 0.0);
972    }
973    
974    /**
975     * Tests this plot for equality with an arbitrary object.
976     * 
977     * @param obj  the object (<code>null</code> permitted).
978     * 
979     * @return A boolean.
980     */
981    public boolean equals(Object obj) {
982        if (obj == this) {
983            return true;
984        }
985        if (!(obj instanceof PiePlot3D)) {
986            return false;
987        }
988        PiePlot3D that = (PiePlot3D) obj;
989        if (this.depthFactor != that.depthFactor) {
990            return false;
991        }
992        if (this.darkerSides != that.darkerSides) {
993            return false;
994        }
995        return super.equals(obj);
996    }
997
998}