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