001    /* ===========================================================
002     * JFreeChart : a free chart library for the Java(tm) platform
003     * ===========================================================
004     *
005     * (C) Copyright 2000-2006, 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-2006, by Object Refinery Limited.
031     *
032     * Original Author:  David Gilbert (for Object Refinery Limtied);
033     * Contributor(s):   -
034     *
035     * $Id: RingPlot.java,v 1.4.2.11 2007/01/17 15:24:31 mungady Exp $
036     *
037     * Changes
038     * -------
039     * 08-Nov-2004 : Version 1 (DG);
040     * 22-Feb-2005 : Renamed DonutPlot --> RingPlot (DG);
041     * 06-Jun-2005 : Added default constructor and fixed equals() method to handle
042     *               GradientPaint (DG);
043     * ------------- JFREECHART 1.0.x ---------------------------------------------
044     * 20-Dec-2005 : Fixed problem with entity shape (bug 1386328) (DG);
045     * 27-Sep-2006 : Updated drawItem() method for new lookup methods (DG);
046     * 12-Oct-2006 : Added configurable section depth (DG);
047     *
048     */
049    
050    package org.jfree.chart.plot;
051    
052    import java.awt.BasicStroke;
053    import java.awt.Color;
054    import java.awt.Graphics2D;
055    import java.awt.Paint;
056    import java.awt.Shape;
057    import java.awt.Stroke;
058    import java.awt.geom.Arc2D;
059    import java.awt.geom.GeneralPath;
060    import java.awt.geom.Line2D;
061    import java.awt.geom.Rectangle2D;
062    import java.io.IOException;
063    import java.io.ObjectInputStream;
064    import java.io.ObjectOutputStream;
065    import java.io.Serializable;
066    
067    import org.jfree.chart.entity.EntityCollection;
068    import org.jfree.chart.entity.PieSectionEntity;
069    import org.jfree.chart.event.PlotChangeEvent;
070    import org.jfree.chart.labels.PieToolTipGenerator;
071    import org.jfree.chart.urls.PieURLGenerator;
072    import org.jfree.data.general.PieDataset;
073    import org.jfree.io.SerialUtilities;
074    import org.jfree.ui.RectangleInsets;
075    import org.jfree.util.ObjectUtilities;
076    import org.jfree.util.PaintUtilities;
077    import org.jfree.util.Rotation;
078    import org.jfree.util.ShapeUtilities;
079    import org.jfree.util.UnitType;
080    
081    /**
082     * A customised pie plot that leaves a hole in the middle.
083     */
084    public 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            notifyListeners(new PlotChangeEvent(this));
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.
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.
205         * 
206         * @param paint  the paint (<code>null</code> not permitted).
207         * 
208         * @see #getSeparatorPaint()
209         */
210        public void setSeparatorPaint(Paint paint) {
211            if (paint == null) {
212                throw new IllegalArgumentException("Null 'paint' argument.");
213            }
214            this.separatorPaint = paint;
215            notifyListeners(new PlotChangeEvent(this));
216        }
217        
218        /**
219         * Returns the length of the inner extension of the separator line that
220         * is drawn between sections, expressed as a percentage of the depth of
221         * the section.
222         * 
223         * @return The inner separator extension (as a percentage).
224         * 
225         * @see #setInnerSeparatorExtension(double)
226         */
227        public double getInnerSeparatorExtension() {
228            return this.innerSeparatorExtension;
229        }
230        
231        /**
232         * Sets the length of the inner extension of the separator line that is
233         * drawn between sections, as a percentage of the depth of the 
234         * sections, and sends a {@link PlotChangeEvent} to all registered 
235         * listeners.
236         * 
237         * @param percent  the percentage.
238         * 
239         * @see #getInnerSeparatorExtension()
240         * @see #setOuterSeparatorExtension(double)
241         */
242        public void setInnerSeparatorExtension(double percent) {
243            this.innerSeparatorExtension = percent;
244            notifyListeners(new PlotChangeEvent(this));
245        }
246        
247        /**
248         * Returns the length of the outer extension of the separator line that
249         * is drawn between sections, expressed as a percentage of the depth of
250         * the section.
251         * 
252         * @return The outer separator extension (as a percentage).
253         * 
254         * @see #setOuterSeparatorExtension(double)
255         */
256        public double getOuterSeparatorExtension() {
257            return this.outerSeparatorExtension;
258        }
259        
260        /**
261         * Sets the length of the outer extension of the separator line that is
262         * drawn between sections, as a percentage of the depth of the 
263         * sections, and sends a {@link PlotChangeEvent} to all registered 
264         * listeners.
265         * 
266         * @param percent  the percentage.
267         * 
268         * @see #getOuterSeparatorExtension()
269         */
270        public void setOuterSeparatorExtension(double percent) {
271            this.outerSeparatorExtension = percent;
272            notifyListeners(new PlotChangeEvent(this));
273        }
274        
275        /**
276         * Returns the depth of each section, expressed as a percentage of the
277         * plot radius.
278         * 
279         * @return The depth of each section.
280         * 
281         * @see #setSectionDepth(double)
282         * @since 1.0.3
283         */
284        public double getSectionDepth() {
285            return this.sectionDepth;
286        }
287        
288        /**
289         * The section depth is given as percentage of the plot radius.
290         * Specifying 1.0 results in a straightforward pie chart.
291         * 
292         * @param sectionDepth  the section depth.
293         *
294         * @see #getSectionDepth()
295         * @since 1.0.3
296         */
297        public void setSectionDepth(double sectionDepth) {
298            this.sectionDepth = sectionDepth;
299        }
300    
301        /**
302         * Initialises the plot state (which will store the total of all dataset
303         * values, among other things).  This method is called once at the 
304         * beginning of each drawing.
305         *
306         * @param g2  the graphics device.
307         * @param plotArea  the plot area (<code>null</code> not permitted).
308         * @param plot  the plot.
309         * @param index  the secondary index (<code>null</code> for primary 
310         *               renderer).
311         * @param info  collects chart rendering information for return to caller.
312         * 
313         * @return A state object (maintains state information relevant to one 
314         *         chart drawing).
315         */
316        public PiePlotState initialise(Graphics2D g2, Rectangle2D plotArea,
317                PiePlot plot, Integer index, PlotRenderingInfo info) {
318    
319            PiePlotState state = super.initialise(g2, plotArea, plot, index, info);
320            state.setPassesRequired(3);
321            return state;   
322    
323        }
324    
325        /**
326         * Draws a single data item.
327         *
328         * @param g2  the graphics device (<code>null</code> not permitted).
329         * @param section  the section index.
330         * @param dataArea  the data plot area.
331         * @param state  state information for one chart.
332         * @param currentPass  the current pass index.
333         */
334        protected void drawItem(Graphics2D g2,
335                                int section,
336                                Rectangle2D dataArea,
337                                PiePlotState state,
338                                int currentPass) {
339        
340            PieDataset dataset = getDataset();
341            Number n = dataset.getValue(section);
342            if (n == null) {
343                return;   
344            }
345            double value = n.doubleValue();
346            double angle1 = 0.0;
347            double angle2 = 0.0;
348            
349            Rotation direction = getDirection();
350            if (direction == Rotation.CLOCKWISE) {
351                angle1 = state.getLatestAngle();
352                angle2 = angle1 - value / state.getTotal() * 360.0;
353            }
354            else if (direction == Rotation.ANTICLOCKWISE) {
355                angle1 = state.getLatestAngle();
356                angle2 = angle1 + value / state.getTotal() * 360.0;         
357            }
358            else {
359                throw new IllegalStateException("Rotation type not recognised.");   
360            }
361            
362            double angle = (angle2 - angle1);
363            if (Math.abs(angle) > getMinimumArcAngleToDraw()) {
364                Comparable key = getSectionKey(section);
365                double ep = 0.0;
366                double mep = getMaximumExplodePercent();
367                if (mep > 0.0) {
368                    ep = getExplodePercent(key) / mep;                
369                }
370                Rectangle2D arcBounds = getArcBounds(state.getPieArea(), 
371                        state.getExplodedPieArea(), angle1, angle, ep);            
372                Arc2D.Double arc = new Arc2D.Double(arcBounds, angle1, angle, 
373                        Arc2D.OPEN);
374    
375                // create the bounds for the inner arc
376                double depth = this.sectionDepth / 2.0;
377                RectangleInsets s = new RectangleInsets(UnitType.RELATIVE, 
378                    depth, depth, depth, depth);
379                Rectangle2D innerArcBounds = new Rectangle2D.Double();
380                innerArcBounds.setRect(arcBounds);
381                s.trim(innerArcBounds);
382                // calculate inner arc in reverse direction, for later 
383                // GeneralPath construction
384                Arc2D.Double arc2 = new Arc2D.Double(innerArcBounds, angle1 
385                        + angle, -angle, Arc2D.OPEN);
386                GeneralPath path = new GeneralPath();
387                path.moveTo((float) arc.getStartPoint().getX(), 
388                        (float) arc.getStartPoint().getY());
389                path.append(arc.getPathIterator(null), false);
390                path.append(arc2.getPathIterator(null), true);
391                path.closePath();
392                
393                Line2D separator = new Line2D.Double(arc2.getEndPoint(), 
394                        arc.getStartPoint());
395                
396                if (currentPass == 0) {
397                    Paint shadowPaint = getShadowPaint();
398                    double shadowXOffset = getShadowXOffset();
399                    double shadowYOffset = getShadowYOffset();
400                    if (shadowPaint != null) {
401                        Shape shadowArc = ShapeUtilities.createTranslatedShape(
402                                path, (float) shadowXOffset, (float) shadowYOffset);
403                        g2.setPaint(shadowPaint);
404                        g2.fill(shadowArc);
405                    }
406                }
407                else if (currentPass == 1) {
408                    Paint paint = lookupSectionPaint(key, true);
409                    g2.setPaint(paint);
410                    g2.fill(path);
411                    Paint outlinePaint = lookupSectionOutlinePaint(key);
412                    Stroke outlineStroke = lookupSectionOutlineStroke(key);
413                    if (outlinePaint != null && outlineStroke != null) {
414                        g2.setPaint(outlinePaint);
415                        g2.setStroke(outlineStroke);
416                        g2.draw(path);
417                    }
418                    
419                    // add an entity for the pie section
420                    if (state.getInfo() != null) {
421                        EntityCollection entities = state.getEntityCollection();
422                        if (entities != null) {
423                            String tip = null;
424                            PieToolTipGenerator toolTipGenerator 
425                                    = getToolTipGenerator();
426                            if (toolTipGenerator != null) {
427                                tip = toolTipGenerator.generateToolTip(dataset, 
428                                        key);
429                            }
430                            String url = null;
431                            PieURLGenerator urlGenerator = getURLGenerator();
432                            if (urlGenerator != null) {
433                                url = urlGenerator.generateURL(dataset, key, 
434                                        getPieIndex());
435                            }
436                            PieSectionEntity entity = new PieSectionEntity(path, 
437                                    dataset, getPieIndex(), section, key, tip, 
438                                    url);
439                            entities.add(entity);
440                        }
441                    }
442                }
443                else if (currentPass == 2) {
444                    if (this.separatorsVisible) {
445                        Line2D extendedSeparator = extendLine(separator,
446                            this.innerSeparatorExtension, 
447                            this.outerSeparatorExtension);
448                        g2.setStroke(this.separatorStroke);
449                        g2.setPaint(this.separatorPaint);
450                        g2.draw(extendedSeparator);
451                    }
452                }
453            }    
454            state.setLatestAngle(angle2);
455        }
456    
457        /**
458         * Tests this plot for equality with an arbitrary object.
459         * 
460         * @param obj  the object to test against (<code>null</code> permitted).
461         * 
462         * @return A boolean.
463         */
464        public boolean equals(Object obj) {
465            if (this == obj) {
466                return true;
467            }
468            if (!(obj instanceof RingPlot)) {
469                return false;
470            }
471            RingPlot that = (RingPlot) obj;
472            if (this.separatorsVisible != that.separatorsVisible) {
473                return false;
474            }
475            if (!ObjectUtilities.equal(this.separatorStroke, 
476                    that.separatorStroke)) {
477                return false;
478            }
479            if (!PaintUtilities.equal(this.separatorPaint, that.separatorPaint)) {
480                return false;
481            }
482            if (this.innerSeparatorExtension != that.innerSeparatorExtension) {
483                return false;
484            }
485            if (this.outerSeparatorExtension != that.outerSeparatorExtension) {
486                return false;
487            }
488            if (this.sectionDepth != that.sectionDepth) {
489                return false;
490            }
491            return super.equals(obj);
492        }
493        
494        /**
495         * Creates a new line by extending an existing line.
496         * 
497         * @param line  the line (<code>null</code> not permitted).
498         * @param startPercent  the amount to extend the line at the start point 
499         *                      end.
500         * @param endPercent  the amount to extend the line at the end point end.
501         * 
502         * @return A new line.
503         */
504        private Line2D extendLine(Line2D line, double startPercent, 
505                                  double endPercent) {
506            if (line == null) {
507                throw new IllegalArgumentException("Null 'line' argument.");
508            }
509            double x1 = line.getX1();
510            double x2 = line.getX2();
511            double deltaX = x2 - x1;
512            double y1 = line.getY1();
513            double y2 = line.getY2();
514            double deltaY = y2 - y1;
515            x1 = x1 - (startPercent * deltaX);
516            y1 = y1 - (startPercent * deltaY);
517            x2 = x2 + (endPercent * deltaX);
518            y2 = y2 + (endPercent * deltaY);
519            return new Line2D.Double(x1, y1, x2, y2);
520        }
521        
522        /**
523         * Provides serialization support.
524         *
525         * @param stream  the output stream.
526         *
527         * @throws IOException  if there is an I/O error.
528         */
529        private void writeObject(ObjectOutputStream stream) throws IOException {
530            stream.defaultWriteObject();
531            SerialUtilities.writeStroke(this.separatorStroke, stream);
532            SerialUtilities.writePaint(this.separatorPaint, stream);
533        }
534    
535        /**
536         * Provides serialization support.
537         *
538         * @param stream  the input stream.
539         *
540         * @throws IOException  if there is an I/O error.
541         * @throws ClassNotFoundException  if there is a classpath problem.
542         */
543        private void readObject(ObjectInputStream stream) 
544            throws IOException, ClassNotFoundException {
545            stream.defaultReadObject();
546            this.separatorStroke = SerialUtilities.readStroke(stream);
547            this.separatorPaint = SerialUtilities.readPaint(stream);
548        }
549        
550    }