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     * XYShapeRenderer.java
029     * --------------------
030     * (C) Copyright 2008, by Andreas Haumer, xS+S and Contributors.
031     *
032     * Original Author:  Martin Hoeller (x Software + Systeme  xS+S - Andreas
033     *                       Haumer);
034     * Contributor(s):   David Gilbert (for Object Refinery Limited);
035     *
036     * Changes:
037     * --------
038     * 17-Sep-2008 : Version 1, based on a contribution from Martin Hoeller with
039     *               amendments by David Gilbert (DG);
040     *
041     */
042    
043    package org.jfree.chart.renderer.xy;
044    
045    import java.awt.BasicStroke;
046    import java.awt.Color;
047    import java.awt.Graphics2D;
048    import java.awt.Paint;
049    import java.awt.Shape;
050    import java.awt.Stroke;
051    import java.awt.geom.Ellipse2D;
052    import java.awt.geom.Line2D;
053    import java.awt.geom.Rectangle2D;
054    import java.io.IOException;
055    import java.io.ObjectInputStream;
056    import java.io.ObjectOutputStream;
057    import java.io.Serializable;
058    
059    import org.jfree.chart.axis.ValueAxis;
060    import org.jfree.chart.entity.EntityCollection;
061    import org.jfree.chart.event.RendererChangeEvent;
062    import org.jfree.chart.plot.CrosshairState;
063    import org.jfree.chart.plot.PlotOrientation;
064    import org.jfree.chart.plot.PlotRenderingInfo;
065    import org.jfree.chart.plot.XYPlot;
066    import org.jfree.chart.renderer.LookupPaintScale;
067    import org.jfree.chart.renderer.PaintScale;
068    import org.jfree.data.Range;
069    import org.jfree.data.general.DatasetUtilities;
070    import org.jfree.data.xy.XYDataset;
071    import org.jfree.data.xy.XYZDataset;
072    import org.jfree.io.SerialUtilities;
073    import org.jfree.util.PublicCloneable;
074    import org.jfree.util.ShapeUtilities;
075    
076    /**
077     * A renderer that draws shapes at (x, y) coordinates and, if the dataset
078     * is an instance of {@link XYZDataset}, fills the shapes with a paint that
079     * is based on the z-value (the paint is obtained from a lookup table).  The
080     * renderer also allows for optional guidelines, horizontal and vertical lines
081     * connecting the shape to the edges of the plot.
082     * <br><br>
083     * The example shown here is generated by the
084     * <code>XYShapeRendererDemo1.java</code> program included in the JFreeChart
085     * demo collection:
086     * <br><br>
087     * <img src="../../../../../images/XYShapeRendererSample.png"
088     * alt="XYShapeRendererSample.png" />
089     * <br><br>
090     * This renderer has similarities to, but also differences from, the
091     * {@link XYLineAndShapeRenderer}.
092     *
093     * @since 1.0.11
094     */
095    public class XYShapeRenderer extends AbstractXYItemRenderer
096            implements XYItemRenderer, Cloneable, Serializable {
097    
098        /** Auto generated serial version id. */
099        private static final long serialVersionUID = 8320552104211173221L;
100    
101        /** The paint scale. */
102        private PaintScale paintScale;
103    
104        /** A flag that controls whether or not the shape outlines are drawn. */
105        private boolean drawOutlines;
106    
107        /**
108         * A flag that controls whether or not the outline paint is used (if not,
109         * the regular paint is used).
110         */
111        private boolean useOutlinePaint;
112    
113        /**
114         * A flag that controls whether or not the fill paint is used (if not,
115         * the fill paint is used).
116         */
117        private boolean useFillPaint;
118    
119        /** Flag indicating if guide lines should be drawn for every item. */
120        private boolean guideLinesVisible;
121    
122        /** The paint used for drawing the guide lines. */
123        private transient Paint guideLinePaint;
124    
125        /** The stroke used for drawing the guide lines. */
126        private transient Stroke guideLineStroke;
127    
128        /**
129         * Creates a new <code>XYShapeRenderer</code> instance with default
130         * attributes.
131         */
132        public XYShapeRenderer() {
133            this.paintScale = new LookupPaintScale();
134            this.useFillPaint = false;
135            this.drawOutlines = false;
136            this.useOutlinePaint = true;
137            this.guideLinesVisible = false;
138            this.guideLinePaint = Color.darkGray;
139            this.guideLineStroke = new BasicStroke();
140            setBaseShape(new Ellipse2D.Double(-5.0, -5.0, 10.0, 10.0));
141            setAutoPopulateSeriesShape(false);
142        }
143    
144        /**
145         * Returns the paint scale used by the renderer.
146         *
147         * @return The paint scale (never <code>null</code>).
148         *
149         * @see #setPaintScale(PaintScale)
150         */
151        public PaintScale getPaintScale() {
152            return this.paintScale;
153        }
154    
155        /**
156         * Sets the paint scale used by the renderer and sends a
157         * {@link RendererChangeEvent} to all registered listeners.
158         *
159         * @param scale  the scale (<code>null</code> not permitted).
160         *
161         * @see #getPaintScale()
162         */
163        public void setPaintScale(PaintScale scale) {
164            if (scale == null) {
165                throw new IllegalArgumentException("Null 'scale' argument.");
166            }
167            this.paintScale = scale;
168            notifyListeners(new RendererChangeEvent(this));
169        }
170    
171        /**
172         * Returns <code>true</code> if outlines should be drawn for shapes, and
173         * <code>false</code> otherwise.
174         *
175         * @return A boolean.
176         *
177         * @see #setDrawOutlines(boolean)
178         */
179        public boolean getDrawOutlines() {
180            return this.drawOutlines;
181        }
182    
183        /**
184         * Sets the flag that controls whether outlines are drawn for
185         * shapes, and sends a {@link RendererChangeEvent} to all registered
186         * listeners.
187         * <P>
188         * In some cases, shapes look better if they do NOT have an outline, but
189         * this flag allows you to set your own preference.
190         *
191         * @param flag  the flag.
192         *
193         * @see #getDrawOutlines()
194         */
195        public void setDrawOutlines(boolean flag) {
196            this.drawOutlines = flag;
197            fireChangeEvent();
198        }
199    
200        /**
201         * Returns <code>true</code> if the renderer should use the fill paint
202         * setting to fill shapes, and <code>false</code> if it should just
203         * use the regular paint.
204         * <p>
205         * Refer to <code>XYLineAndShapeRendererDemo2.java</code> to see the
206         * effect of this flag.
207         *
208         * @return A boolean.
209         *
210         * @see #setUseFillPaint(boolean)
211         * @see #getUseOutlinePaint()
212         */
213        public boolean getUseFillPaint() {
214            return this.useFillPaint;
215        }
216    
217        /**
218         * Sets the flag that controls whether the fill paint is used to fill
219         * shapes, and sends a {@link RendererChangeEvent} to all
220         * registered listeners.
221         *
222         * @param flag  the flag.
223         *
224         * @see #getUseFillPaint()
225         */
226        public void setUseFillPaint(boolean flag) {
227            this.useFillPaint = flag;
228            fireChangeEvent();
229        }
230    
231        /**
232         * Returns the flag that controls whether the outline paint is used for
233         * shape outlines.  If not, the regular series paint is used.
234         *
235         * @return A boolean.
236         *
237         * @see #setUseOutlinePaint(boolean)
238         */
239        public boolean getUseOutlinePaint() {
240            return this.useOutlinePaint;
241        }
242    
243        /**
244         * Sets the flag that controls whether the outline paint is used for shape
245         * outlines, and sends a {@link RendererChangeEvent} to all registered
246         * listeners.
247         *
248         * @param use  the flag.
249         *
250         * @see #getUseOutlinePaint()
251         */
252        public void setUseOutlinePaint(boolean use) {
253            this.useOutlinePaint = use;
254            fireChangeEvent();
255        }
256    
257        /**
258         * Returns a flag that controls whether or not guide lines are drawn for
259         * each data item (the lines are horizontal and vertical "crosshairs"
260         * linking the data point to the axes).
261         *
262         * @return A boolean.
263         *
264         * @see #setGuideLinesVisible(boolean)
265         */
266        public boolean isGuideLinesVisible() {
267            return this.guideLinesVisible;
268        }
269    
270        /**
271         * Sets the flag that controls whether or not guide lines are drawn for
272         * each data item and sends a {@link RendererChangeEvent} to all registered
273         * listeners.
274         *
275         * @param visible  the new flag value.
276         *
277         * @see #isGuideLinesVisible()
278         */
279        public void setGuideLinesVisible(boolean visible) {
280            this.guideLinesVisible = visible;
281            fireChangeEvent();
282        }
283    
284        /**
285         * Returns the paint used to draw the guide lines.
286         *
287         * @return The paint (never <code>null</code>).
288         *
289         * @see #setGuideLinePaint(Paint)
290         */
291        public Paint getGuideLinePaint() {
292            return this.guideLinePaint;
293        }
294    
295        /**
296         * Sets the paint used to draw the guide lines and sends a
297         * {@link RendererChangeEvent} to all registered listeners.
298         *
299         * @param paint  the paint (<code>null</code> not permitted).
300         *
301         * @see #getGuideLinePaint()
302         */
303        public void setGuideLinePaint(Paint paint) {
304            if (paint == null) {
305                throw new IllegalArgumentException("Null 'paint' argument.");
306            }
307            this.guideLinePaint = paint;
308            fireChangeEvent();
309        }
310    
311        /**
312         * Returns the stroke used to draw the guide lines.
313         *
314         * @return The stroke.
315         *
316         * @see #setGuideLineStroke(Stroke)
317         */
318        public Stroke getGuideLineStroke() {
319            return this.guideLineStroke;
320        }
321    
322        /**
323         * Sets the stroke used to draw the guide lines and sends a
324         * {@link RendererChangeEvent} to all registered listeners.
325         *
326         * @param stroke  the stroke (<code>null</code> not permitted).
327         *
328         * @see #getGuideLineStroke()
329         */
330        public void setGuideLineStroke(Stroke stroke) {
331            if (stroke == null) {
332                throw new IllegalArgumentException("Null 'stroke' argument.");
333            }
334            this.guideLineStroke = stroke;
335            fireChangeEvent();
336        }
337    
338        /**
339         * Returns the lower and upper bounds (range) of the x-values in the
340         * specified dataset.
341         *
342         * @param dataset  the dataset (<code>null</code> permitted).
343         *
344         * @return The range (<code>null</code> if the dataset is <code>null</code>
345         *         or empty).
346         */
347        public Range findDomainBounds(XYDataset dataset) {
348            if (dataset != null) {
349                Range r = DatasetUtilities.findDomainBounds(dataset, false);
350                double offset = 0; // TODO getSeriesShape(n).getBounds().width / 2;
351                return new Range(r.getLowerBound() + offset,
352                                 r.getUpperBound() + offset);
353            }
354            else {
355                return null;
356            }
357        }
358    
359        /**
360         * Returns the range of values the renderer requires to display all the
361         * items from the specified dataset.
362         *
363         * @param dataset  the dataset (<code>null</code> permitted).
364         *
365         * @return The range (<code>null</code> if the dataset is <code>null</code>
366         *         or empty).
367         */
368        public Range findRangeBounds(XYDataset dataset) {
369            if (dataset != null) {
370                Range r = DatasetUtilities.findRangeBounds(dataset, false);
371                double offset = 0; // TODO getSeriesShape(n).getBounds().height / 2;
372                return new Range(r.getLowerBound() + offset, r.getUpperBound()
373                        + offset);
374            }
375            else {
376                return null;
377            }
378        }
379    
380        /**
381         * Returns the number of passes required by this renderer.
382         *
383         * @return <code>2</code>.
384         */
385        public int getPassCount() {
386            return 2;
387        }
388    
389        /**
390         * Draws the block representing the specified item.
391         *
392         * @param g2  the graphics device.
393         * @param state  the state.
394         * @param dataArea  the data area.
395         * @param info  the plot rendering info.
396         * @param plot  the plot.
397         * @param domainAxis  the x-axis.
398         * @param rangeAxis  the y-axis.
399         * @param dataset  the dataset.
400         * @param series  the series index.
401         * @param item  the item index.
402         * @param crosshairState  the crosshair state.
403         * @param pass  the pass index.
404         */
405        public void drawItem(Graphics2D g2, XYItemRendererState state,
406                Rectangle2D dataArea, PlotRenderingInfo info, XYPlot plot,
407                ValueAxis domainAxis, ValueAxis rangeAxis, XYDataset dataset,
408                int series, int item, CrosshairState crosshairState, int pass) {
409    
410            Shape hotspot = null;
411            EntityCollection entities = null;
412            if (info != null) {
413                entities = info.getOwner().getEntityCollection();
414            }
415    
416            double x = dataset.getXValue(series, item);
417            double y = dataset.getYValue(series, item);
418            if (Double.isNaN(x) || Double.isNaN(y)) {
419                // can't draw anything
420                return;
421            }
422    
423            double transX = domainAxis.valueToJava2D(x, dataArea,
424                    plot.getDomainAxisEdge());
425            double transY = rangeAxis.valueToJava2D(y, dataArea,
426                    plot.getRangeAxisEdge());
427    
428            PlotOrientation orientation = plot.getOrientation();
429    
430            // draw optional guide lines
431            if ((pass == 0) && this.guideLinesVisible) {
432                g2.setStroke(this.guideLineStroke);
433                g2.setPaint(this.guideLinePaint);
434                if (orientation == PlotOrientation.HORIZONTAL) {
435                    g2.draw(new Line2D.Double(transY, dataArea.getMinY(), transY,
436                            dataArea.getMaxY()));
437                    g2.draw(new Line2D.Double(dataArea.getMinX(), transX,
438                            dataArea.getMaxX(), transX));
439                }
440                else {
441                    g2.draw(new Line2D.Double(transX, dataArea.getMinY(), transX,
442                            dataArea.getMaxY()));
443                    g2.draw(new Line2D.Double(dataArea.getMinX(), transY,
444                            dataArea.getMaxX(), transY));
445                }
446            }
447            else if (pass == 1) {
448                Shape shape = getItemShape(series, item);
449                if (orientation == PlotOrientation.HORIZONTAL) {
450                    shape = ShapeUtilities.createTranslatedShape(shape, transY,
451                            transX);
452                }
453                else if (orientation == PlotOrientation.VERTICAL) {
454                    shape = ShapeUtilities.createTranslatedShape(shape, transX,
455                            transY);
456                }
457                hotspot = shape;
458                if (shape.intersects(dataArea)) {
459                    //if (getItemShapeFilled(series, item)) {
460                        g2.setPaint(getPaint(dataset, series, item));
461                        g2.fill(shape);
462                   //}
463                    if (this.drawOutlines) {
464                        if (getUseOutlinePaint()) {
465                            g2.setPaint(getItemOutlinePaint(series, item));
466                        }
467                        else {
468                            g2.setPaint(getItemPaint(series, item));
469                        }
470                        g2.setStroke(getItemOutlineStroke(series, item));
471                        g2.draw(shape);
472                    }
473                }
474    
475                // add an entity for the item...
476                if (entities != null) {
477                    addEntity(entities, hotspot, dataset, series, item, transX,
478                            transY);
479                }
480            }
481        }
482    
483        /**
484         * Get the paint for a given series and item from a dataset.
485         *
486         * @param dataset  the dataset..
487         * @param series  the series index.
488         * @param item  the item index.
489         *
490         * @return The paint.
491         */
492        protected Paint getPaint(XYDataset dataset, int series, int item) {
493            Paint p = null;
494            if (dataset instanceof XYZDataset) {
495                double z = ((XYZDataset) dataset).getZValue(series, item);
496                p = this.paintScale.getPaint(z);
497            }
498            else {
499                if (this.useFillPaint) {
500                    p = getItemFillPaint(series, item);
501                }
502                else {
503                    p = getItemPaint(series, item);
504                }
505            }
506            return p;
507        }
508    
509        /**
510         * Tests this instance for equality with an arbitrary object.  This method
511         * returns <code>true</code> if and only if:
512         * <ul>
513         * <li><code>obj</code> is an instance of <code>XYShapeRenderer</code> (not
514         *     <code>null</code>);</li>
515         * <li><code>obj</code> has the same field values as this
516         *     <code>XYShapeRenderer</code>;</li>
517         * </ul>
518         *
519         * @param obj  the object (<code>null</code> permitted).
520         *
521         * @return A boolean.
522         */
523        public boolean equals(Object obj) {
524            if (obj == this) {
525                return true;
526            }
527            if (!(obj instanceof XYShapeRenderer)) {
528                return false;
529            }
530            XYShapeRenderer that = (XYShapeRenderer) obj;
531            if ((this.paintScale == null && that.paintScale != null)
532                    || (!this.paintScale.equals(that.paintScale))) {
533                return false;
534            }
535            if (this.drawOutlines != that.drawOutlines) {
536                return false;
537            }
538            if (this.useOutlinePaint != that.useOutlinePaint) {
539                return false;
540            }
541            if (this.useFillPaint != that.useFillPaint) {
542                return false;
543            }
544            if (this.guideLinesVisible != that.guideLinesVisible) {
545                return false;
546            }
547            if ((this.guideLinePaint == null && that.guideLinePaint != null)
548                    || (!this.guideLinePaint.equals(that.guideLinePaint)))
549                return false;
550            if ((this.guideLineStroke == null && that.guideLineStroke != null)
551                    || (!this.guideLineStroke.equals(that.guideLineStroke)))
552                return false;
553    
554            return super.equals(obj);
555        }
556    
557        /**
558         * Returns a clone of this renderer.
559         *
560         * @return A clone of this renderer.
561         *
562         * @throws CloneNotSupportedException if there is a problem creating the
563         *     clone.
564         */
565        public Object clone() throws CloneNotSupportedException {
566            XYShapeRenderer clone = (XYShapeRenderer) super.clone();
567            if (this.paintScale instanceof PublicCloneable) {
568                PublicCloneable pc = (PublicCloneable) this.paintScale;
569                clone.paintScale = (PaintScale) pc.clone();
570            }
571            return clone;
572        }
573    
574        /**
575         * Provides serialization support.
576         *
577         * @param stream  the input stream.
578         *
579         * @throws IOException  if there is an I/O error.
580         * @throws ClassNotFoundException  if there is a classpath problem.
581         */
582        private void readObject(ObjectInputStream stream)
583                throws IOException, ClassNotFoundException {
584            stream.defaultReadObject();
585            this.guideLinePaint = SerialUtilities.readPaint(stream);
586            this.guideLineStroke = SerialUtilities.readStroke(stream);
587        }
588    
589        /**
590         * Provides serialization support.
591         *
592         * @param stream  the output stream.
593         *
594         * @throws IOException  if there is an I/O error.
595         */
596        private void writeObject(ObjectOutputStream stream) throws IOException {
597            stream.defaultWriteObject();
598            SerialUtilities.writePaint(this.guideLinePaint, stream);
599            SerialUtilities.writeStroke(this.guideLineStroke, stream);
600        }
601    
602    }