001    /* ===========================================================
002     * JFreeChart : a free chart library for the Java(tm) platform
003     * ===========================================================
004     *
005     * (C) Copyright 2000-2011, 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     * [Oracle and Java are registered trademarks of Oracle and/or its affiliates. 
025     * Other names may be trademarks of their respective owners.]
026     *
027     * -------------
028     * RingPlot.java
029     * -------------
030     * (C) Copyright 2004-2011, by Object Refinery Limited.
031     *
032     * Original Author:  David Gilbert (for Object Refinery Limtied);
033     * Contributor(s):   Christoph Beck (bug 2121818);
034     *
035     * Changes
036     * -------
037     * 08-Nov-2004 : Version 1 (DG);
038     * 22-Feb-2005 : Renamed DonutPlot --> RingPlot (DG);
039     * 06-Jun-2005 : Added default constructor and fixed equals() method to handle
040     *               GradientPaint (DG);
041     * ------------- JFREECHART 1.0.x ---------------------------------------------
042     * 20-Dec-2005 : Fixed problem with entity shape (bug 1386328) (DG);
043     * 27-Sep-2006 : Updated drawItem() method for new lookup methods (DG);
044     * 12-Oct-2006 : Added configurable section depth (DG);
045     * 14-Feb-2007 : Added notification in setSectionDepth() method (DG);
046     * 23-Sep-2008 : Fix for bug 2121818 by Christoph Beck (DG);
047     * 13-Jul-2009 : Added support for shadow generator (DG);
048     * 11-Oct-2011 : Check sectionOutlineVisible - bug 3237879 (DG);
049     *
050     */
051    
052    package org.jfree.chart.plot;
053    
054    import java.awt.BasicStroke;
055    import java.awt.Color;
056    import java.awt.Graphics2D;
057    import java.awt.Paint;
058    import java.awt.Shape;
059    import java.awt.Stroke;
060    import java.awt.geom.Arc2D;
061    import java.awt.geom.GeneralPath;
062    import java.awt.geom.Line2D;
063    import java.awt.geom.Rectangle2D;
064    import java.io.IOException;
065    import java.io.ObjectInputStream;
066    import java.io.ObjectOutputStream;
067    import java.io.Serializable;
068    
069    import org.jfree.chart.entity.EntityCollection;
070    import org.jfree.chart.entity.PieSectionEntity;
071    import org.jfree.chart.event.PlotChangeEvent;
072    import org.jfree.chart.labels.PieToolTipGenerator;
073    import org.jfree.chart.urls.PieURLGenerator;
074    import org.jfree.data.general.PieDataset;
075    import org.jfree.io.SerialUtilities;
076    import org.jfree.ui.RectangleInsets;
077    import org.jfree.util.ObjectUtilities;
078    import org.jfree.util.PaintUtilities;
079    import org.jfree.util.Rotation;
080    import org.jfree.util.ShapeUtilities;
081    import org.jfree.util.UnitType;
082    
083    /**
084     * A customised pie plot that leaves a hole in the middle.
085     */
086    public class RingPlot extends PiePlot implements Cloneable, Serializable {
087    
088        /** For serialization. */
089        private static final long serialVersionUID = 1556064784129676620L;
090    
091        /**
092         * A flag that controls whether or not separators are drawn between the
093         * sections of the chart.
094         */
095        private boolean separatorsVisible;
096    
097        /** The stroke used to draw separators. */
098        private transient Stroke separatorStroke;
099    
100        /** The paint used to draw separators. */
101        private transient Paint separatorPaint;
102    
103        /**
104         * The length of the inner separator extension (as a percentage of the
105         * depth of the sections).
106         */
107        private double innerSeparatorExtension;
108    
109        /**
110         * The length of the outer separator extension (as a percentage of the
111         * depth of the sections).
112         */
113        private double outerSeparatorExtension;
114    
115        /**
116         * The depth of the section as a percentage of the diameter.
117         */
118        private double sectionDepth;
119    
120        /**
121         * Creates a new plot with a <code>null</code> dataset.
122         */
123        public RingPlot() {
124            this(null);
125        }
126    
127        /**
128         * Creates a new plot for the specified dataset.
129         *
130         * @param dataset  the dataset (<code>null</code> permitted).
131         */
132        public RingPlot(PieDataset dataset) {
133            super(dataset);
134            this.separatorsVisible = true;
135            this.separatorStroke = new BasicStroke(0.5f);
136            this.separatorPaint = Color.gray;
137            this.innerSeparatorExtension = 0.20;  // twenty percent
138            this.outerSeparatorExtension = 0.20;  // twenty percent
139            this.sectionDepth = 0.20; // 20%
140        }
141    
142        /**
143         * Returns a flag that indicates whether or not separators are drawn between
144         * the sections in the chart.
145         *
146         * @return A boolean.
147         *
148         * @see #setSeparatorsVisible(boolean)
149         */
150        public boolean getSeparatorsVisible() {
151            return this.separatorsVisible;
152        }
153    
154        /**
155         * Sets the flag that controls whether or not separators are drawn between
156         * the sections in the chart, and sends a {@link PlotChangeEvent} to all
157         * registered listeners.
158         *
159         * @param visible  the flag.
160         *
161         * @see #getSeparatorsVisible()
162         */
163        public void setSeparatorsVisible(boolean visible) {
164            this.separatorsVisible = visible;
165            fireChangeEvent();
166        }
167    
168        /**
169         * Returns the separator stroke.
170         *
171         * @return The stroke (never <code>null</code>).
172         *
173         * @see #setSeparatorStroke(Stroke)
174         */
175        public Stroke getSeparatorStroke() {
176            return this.separatorStroke;
177        }
178    
179        /**
180         * Sets the stroke used to draw the separator between sections and sends
181         * a {@link PlotChangeEvent} to all registered listeners.
182         *
183         * @param stroke  the stroke (<code>null</code> not permitted).
184         *
185         * @see #getSeparatorStroke()
186         */
187        public void setSeparatorStroke(Stroke stroke) {
188            if (stroke == null) {
189                throw new IllegalArgumentException("Null 'stroke' argument.");
190            }
191            this.separatorStroke = stroke;
192            fireChangeEvent();
193        }
194    
195        /**
196         * Returns the separator paint.
197         *
198         * @return The paint (never <code>null</code>).
199         *
200         * @see #setSeparatorPaint(Paint)
201         */
202        public Paint getSeparatorPaint() {
203            return this.separatorPaint;
204        }
205    
206        /**
207         * Sets the paint used to draw the separator between sections and sends a
208         * {@link PlotChangeEvent} to all registered listeners.
209         *
210         * @param paint  the paint (<code>null</code> not permitted).
211         *
212         * @see #getSeparatorPaint()
213         */
214        public void setSeparatorPaint(Paint paint) {
215            if (paint == null) {
216                throw new IllegalArgumentException("Null 'paint' argument.");
217            }
218            this.separatorPaint = paint;
219            fireChangeEvent();
220        }
221    
222        /**
223         * Returns the length of the inner extension of the separator line that
224         * is drawn between sections, expressed as a percentage of the depth of
225         * the section.
226         *
227         * @return The inner separator extension (as a percentage).
228         *
229         * @see #setInnerSeparatorExtension(double)
230         */
231        public double getInnerSeparatorExtension() {
232            return this.innerSeparatorExtension;
233        }
234    
235        /**
236         * Sets the length of the inner extension of the separator line that is
237         * drawn between sections, as a percentage of the depth of the
238         * sections, and sends a {@link PlotChangeEvent} to all registered
239         * listeners.
240         *
241         * @param percent  the percentage.
242         *
243         * @see #getInnerSeparatorExtension()
244         * @see #setOuterSeparatorExtension(double)
245         */
246        public void setInnerSeparatorExtension(double percent) {
247            this.innerSeparatorExtension = percent;
248            fireChangeEvent();
249        }
250    
251        /**
252         * Returns the length of the outer extension of the separator line that
253         * is drawn between sections, expressed as a percentage of the depth of
254         * the section.
255         *
256         * @return The outer separator extension (as a percentage).
257         *
258         * @see #setOuterSeparatorExtension(double)
259         */
260        public double getOuterSeparatorExtension() {
261            return this.outerSeparatorExtension;
262        }
263    
264        /**
265         * Sets the length of the outer extension of the separator line that is
266         * drawn between sections, as a percentage of the depth of the
267         * sections, and sends a {@link PlotChangeEvent} to all registered
268         * listeners.
269         *
270         * @param percent  the percentage.
271         *
272         * @see #getOuterSeparatorExtension()
273         */
274        public void setOuterSeparatorExtension(double percent) {
275            this.outerSeparatorExtension = percent;
276            fireChangeEvent();
277        }
278    
279        /**
280         * Returns the depth of each section, expressed as a percentage of the
281         * plot radius.
282         *
283         * @return The depth of each section.
284         *
285         * @see #setSectionDepth(double)
286         * @since 1.0.3
287         */
288        public double getSectionDepth() {
289            return this.sectionDepth;
290        }
291    
292        /**
293         * The section depth is given as percentage of the plot radius.
294         * Specifying 1.0 results in a straightforward pie chart.
295         *
296         * @param sectionDepth  the section depth.
297         *
298         * @see #getSectionDepth()
299         * @since 1.0.3
300         */
301        public void setSectionDepth(double sectionDepth) {
302            this.sectionDepth = sectionDepth;
303            fireChangeEvent();
304        }
305    
306        /**
307         * Initialises the plot state (which will store the total of all dataset
308         * values, among other things).  This method is called once at the
309         * beginning of each drawing.
310         *
311         * @param g2  the graphics device.
312         * @param plotArea  the plot area (<code>null</code> not permitted).
313         * @param plot  the plot.
314         * @param index  the secondary index (<code>null</code> for primary
315         *               renderer).
316         * @param info  collects chart rendering information for return to caller.
317         *
318         * @return A state object (maintains state information relevant to one
319         *         chart drawing).
320         */
321        public PiePlotState initialise(Graphics2D g2, Rectangle2D plotArea,
322                PiePlot plot, Integer index, PlotRenderingInfo info) {
323    
324            PiePlotState state = super.initialise(g2, plotArea, plot, index, info);
325            state.setPassesRequired(3);
326            return state;
327    
328        }
329    
330        /**
331         * Draws a single data item.
332         *
333         * @param g2  the graphics device (<code>null</code> not permitted).
334         * @param section  the section index.
335         * @param dataArea  the data plot area.
336         * @param state  state information for one chart.
337         * @param currentPass  the current pass index.
338         */
339        protected void drawItem(Graphics2D g2,
340                                int section,
341                                Rectangle2D dataArea,
342                                PiePlotState state,
343                                int currentPass) {
344    
345            PieDataset dataset = getDataset();
346            Number n = dataset.getValue(section);
347            if (n == null) {
348                return;
349            }
350            double value = n.doubleValue();
351            double angle1 = 0.0;
352            double angle2 = 0.0;
353    
354            Rotation direction = getDirection();
355            if (direction == Rotation.CLOCKWISE) {
356                angle1 = state.getLatestAngle();
357                angle2 = angle1 - value / state.getTotal() * 360.0;
358            }
359            else if (direction == Rotation.ANTICLOCKWISE) {
360                angle1 = state.getLatestAngle();
361                angle2 = angle1 + value / state.getTotal() * 360.0;
362            }
363            else {
364                throw new IllegalStateException("Rotation type not recognised.");
365            }
366    
367            double angle = (angle2 - angle1);
368            if (Math.abs(angle) > getMinimumArcAngleToDraw()) {
369                Comparable key = getSectionKey(section);
370                double ep = 0.0;
371                double mep = getMaximumExplodePercent();
372                if (mep > 0.0) {
373                    ep = getExplodePercent(key) / mep;
374                }
375                Rectangle2D arcBounds = getArcBounds(state.getPieArea(),
376                        state.getExplodedPieArea(), angle1, angle, ep);
377                Arc2D.Double arc = new Arc2D.Double(arcBounds, angle1, angle,
378                        Arc2D.OPEN);
379    
380                // create the bounds for the inner arc
381                double depth = this.sectionDepth / 2.0;
382                RectangleInsets s = new RectangleInsets(UnitType.RELATIVE,
383                    depth, depth, depth, depth);
384                Rectangle2D innerArcBounds = new Rectangle2D.Double();
385                innerArcBounds.setRect(arcBounds);
386                s.trim(innerArcBounds);
387                // calculate inner arc in reverse direction, for later
388                // GeneralPath construction
389                Arc2D.Double arc2 = new Arc2D.Double(innerArcBounds, angle1
390                        + angle, -angle, Arc2D.OPEN);
391                GeneralPath path = new GeneralPath();
392                path.moveTo((float) arc.getStartPoint().getX(),
393                        (float) arc.getStartPoint().getY());
394                path.append(arc.getPathIterator(null), false);
395                path.append(arc2.getPathIterator(null), true);
396                path.closePath();
397    
398                Line2D separator = new Line2D.Double(arc2.getEndPoint(),
399                        arc.getStartPoint());
400    
401                if (currentPass == 0) {
402                    Paint shadowPaint = getShadowPaint();
403                    double shadowXOffset = getShadowXOffset();
404                    double shadowYOffset = getShadowYOffset();
405                    if (shadowPaint != null && getShadowGenerator() == null) {
406                        Shape shadowArc = ShapeUtilities.createTranslatedShape(
407                                path, (float) shadowXOffset, (float) shadowYOffset);
408                        g2.setPaint(shadowPaint);
409                        g2.fill(shadowArc);
410                    }
411                }
412                else if (currentPass == 1) {
413                    Paint paint = lookupSectionPaint(key);
414                    g2.setPaint(paint);
415                    g2.fill(path);
416                    Paint outlinePaint = lookupSectionOutlinePaint(key);
417                    Stroke outlineStroke = lookupSectionOutlineStroke(key);
418                    if (getSectionOutlinesVisible() && outlinePaint != null 
419                            && outlineStroke != null) {
420                        g2.setPaint(outlinePaint);
421                        g2.setStroke(outlineStroke);
422                        g2.draw(path);
423                    }
424    
425                    // add an entity for the pie section
426                    if (state.getInfo() != null) {
427                        EntityCollection entities = state.getEntityCollection();
428                        if (entities != null) {
429                            String tip = null;
430                            PieToolTipGenerator toolTipGenerator
431                                    = getToolTipGenerator();
432                            if (toolTipGenerator != null) {
433                                tip = toolTipGenerator.generateToolTip(dataset,
434                                        key);
435                            }
436                            String url = null;
437                            PieURLGenerator urlGenerator = getURLGenerator();
438                            if (urlGenerator != null) {
439                                url = urlGenerator.generateURL(dataset, key,
440                                        getPieIndex());
441                            }
442                            PieSectionEntity entity = new PieSectionEntity(path,
443                                    dataset, getPieIndex(), section, key, tip,
444                                    url);
445                            entities.add(entity);
446                        }
447                    }
448                }
449                else if (currentPass == 2) {
450                    if (this.separatorsVisible) {
451                        Line2D extendedSeparator = extendLine(separator,
452                            this.innerSeparatorExtension,
453                            this.outerSeparatorExtension);
454                        g2.setStroke(this.separatorStroke);
455                        g2.setPaint(this.separatorPaint);
456                        g2.draw(extendedSeparator);
457                    }
458                }
459            }
460            state.setLatestAngle(angle2);
461        }
462    
463        /**
464         * This method overrides the default value for cases where the ring plot
465         * is very thin.  This fixes bug 2121818.
466         *
467         * @return The label link depth, as a percentage of the plot's radius.
468         */
469        protected double getLabelLinkDepth() {
470            return Math.min(super.getLabelLinkDepth(), getSectionDepth() / 2);
471        }
472    
473        /**
474         * Tests this plot for equality with an arbitrary object.
475         *
476         * @param obj  the object to test against (<code>null</code> permitted).
477         *
478         * @return A boolean.
479         */
480        public boolean equals(Object obj) {
481            if (this == obj) {
482                return true;
483            }
484            if (!(obj instanceof RingPlot)) {
485                return false;
486            }
487            RingPlot that = (RingPlot) obj;
488            if (this.separatorsVisible != that.separatorsVisible) {
489                return false;
490            }
491            if (!ObjectUtilities.equal(this.separatorStroke,
492                    that.separatorStroke)) {
493                return false;
494            }
495            if (!PaintUtilities.equal(this.separatorPaint, that.separatorPaint)) {
496                return false;
497            }
498            if (this.innerSeparatorExtension != that.innerSeparatorExtension) {
499                return false;
500            }
501            if (this.outerSeparatorExtension != that.outerSeparatorExtension) {
502                return false;
503            }
504            if (this.sectionDepth != that.sectionDepth) {
505                return false;
506            }
507            return super.equals(obj);
508        }
509    
510        /**
511         * Creates a new line by extending an existing line.
512         *
513         * @param line  the line (<code>null</code> not permitted).
514         * @param startPercent  the amount to extend the line at the start point
515         *                      end.
516         * @param endPercent  the amount to extend the line at the end point end.
517         *
518         * @return A new line.
519         */
520        private Line2D extendLine(Line2D line, double startPercent,
521                                  double endPercent) {
522            if (line == null) {
523                throw new IllegalArgumentException("Null 'line' argument.");
524            }
525            double x1 = line.getX1();
526            double x2 = line.getX2();
527            double deltaX = x2 - x1;
528            double y1 = line.getY1();
529            double y2 = line.getY2();
530            double deltaY = y2 - y1;
531            x1 = x1 - (startPercent * deltaX);
532            y1 = y1 - (startPercent * deltaY);
533            x2 = x2 + (endPercent * deltaX);
534            y2 = y2 + (endPercent * deltaY);
535            return new Line2D.Double(x1, y1, x2, y2);
536        }
537    
538        /**
539         * Provides serialization support.
540         *
541         * @param stream  the output stream.
542         *
543         * @throws IOException  if there is an I/O error.
544         */
545        private void writeObject(ObjectOutputStream stream) throws IOException {
546            stream.defaultWriteObject();
547            SerialUtilities.writeStroke(this.separatorStroke, stream);
548            SerialUtilities.writePaint(this.separatorPaint, stream);
549        }
550    
551        /**
552         * Provides serialization support.
553         *
554         * @param stream  the input stream.
555         *
556         * @throws IOException  if there is an I/O error.
557         * @throws ClassNotFoundException  if there is a classpath problem.
558         */
559        private void readObject(ObjectInputStream stream)
560            throws IOException, ClassNotFoundException {
561            stream.defaultReadObject();
562            this.separatorStroke = SerialUtilities.readStroke(stream);
563            this.separatorPaint = SerialUtilities.readPaint(stream);
564        }
565    
566    }