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