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
043package org.jfree.chart.renderer.xy;
044
045import java.awt.BasicStroke;
046import java.awt.Color;
047import java.awt.Graphics2D;
048import java.awt.Paint;
049import java.awt.Shape;
050import java.awt.Stroke;
051import java.awt.geom.Ellipse2D;
052import java.awt.geom.Line2D;
053import java.awt.geom.Rectangle2D;
054import java.io.IOException;
055import java.io.ObjectInputStream;
056import java.io.ObjectOutputStream;
057import java.io.Serializable;
058
059import org.jfree.chart.axis.ValueAxis;
060import org.jfree.chart.entity.EntityCollection;
061import org.jfree.chart.event.RendererChangeEvent;
062import org.jfree.chart.plot.CrosshairState;
063import org.jfree.chart.plot.PlotOrientation;
064import org.jfree.chart.plot.PlotRenderingInfo;
065import org.jfree.chart.plot.XYPlot;
066import org.jfree.chart.renderer.LookupPaintScale;
067import org.jfree.chart.renderer.PaintScale;
068import org.jfree.data.Range;
069import org.jfree.data.general.DatasetUtilities;
070import org.jfree.data.xy.XYDataset;
071import org.jfree.data.xy.XYZDataset;
072import org.jfree.io.SerialUtilities;
073import org.jfree.util.PublicCloneable;
074import 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 */
095public 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}