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