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
052package org.jfree.chart.plot;
053
054import java.awt.BasicStroke;
055import java.awt.Color;
056import java.awt.Graphics2D;
057import java.awt.Paint;
058import java.awt.Shape;
059import java.awt.Stroke;
060import java.awt.geom.Arc2D;
061import java.awt.geom.GeneralPath;
062import java.awt.geom.Line2D;
063import java.awt.geom.Rectangle2D;
064import java.io.IOException;
065import java.io.ObjectInputStream;
066import java.io.ObjectOutputStream;
067import java.io.Serializable;
068
069import org.jfree.chart.entity.EntityCollection;
070import org.jfree.chart.entity.PieSectionEntity;
071import org.jfree.chart.event.PlotChangeEvent;
072import org.jfree.chart.labels.PieToolTipGenerator;
073import org.jfree.chart.urls.PieURLGenerator;
074import org.jfree.data.general.PieDataset;
075import org.jfree.io.SerialUtilities;
076import org.jfree.ui.RectangleInsets;
077import org.jfree.util.ObjectUtilities;
078import org.jfree.util.PaintUtilities;
079import org.jfree.util.Rotation;
080import org.jfree.util.ShapeUtilities;
081import org.jfree.util.UnitType;
082
083/**
084 * A customised pie plot that leaves a hole in the middle.
085 */
086public 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}