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