001/* ===========================================================
002 * JFreeChart : a free chart library for the Java(tm) platform
003 * ===========================================================
004 *
005 * (C) Copyright 2000-2007, 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 * XYStepAreaRenderer.java
029 * -----------------------
030 * (C) Copyright 2003-2007, by Matthias Rose and Contributors.
031 *
032 * Original Author:  Matthias Rose (based on XYAreaRenderer.java);
033 * Contributor(s):   David Gilbert (for Object Refinery Limited);
034 *
035 * Changes:
036 * --------
037 * 07-Oct-2003 : Version 1, contributed by Matthias Rose (DG);
038 * 10-Feb-2004 : Added some getter and setter methods (DG);
039 * 25-Feb-2004 : Replaced CrosshairInfo with CrosshairState.  Renamed 
040 *               XYToolTipGenerator --> XYItemLabelGenerator (DG);
041 * 15-Jul-2004 : Switched getX() with getXValue() and getY() with 
042 *               getYValue() (DG);
043 * 11-Nov-2004 : Now uses ShapeUtilities to translate shapes (DG);
044 * 06-Jul-2005 : Renamed get/setPlotShapes() --> get/setShapesVisible() (DG);
045 * ------------- JFREECHART 1.0.x ---------------------------------------------
046 * 06-Jul-2006 : Modified to call dataset methods that return double 
047 *               primitives only (DG);
048 * 06-Feb-2007 : Fixed bug 1086307, crosshairs with multiple axes (DG);
049 * 14-Feb-2007 : Added equals() method override (DG);
050 * 04-May-2007 : Set processVisibleItemsOnly flag to false (DG);
051 * 
052 */
053
054package org.jfree.chart.renderer.xy;
055
056import java.awt.Graphics2D;
057import java.awt.Paint;
058import java.awt.Polygon;
059import java.awt.Shape;
060import java.awt.Stroke;
061import java.awt.geom.Rectangle2D;
062import java.io.Serializable;
063
064import org.jfree.chart.axis.ValueAxis;
065import org.jfree.chart.entity.EntityCollection;
066import org.jfree.chart.entity.XYItemEntity;
067import org.jfree.chart.event.RendererChangeEvent;
068import org.jfree.chart.labels.XYToolTipGenerator;
069import org.jfree.chart.plot.CrosshairState;
070import org.jfree.chart.plot.PlotOrientation;
071import org.jfree.chart.plot.PlotRenderingInfo;
072import org.jfree.chart.plot.XYPlot;
073import org.jfree.chart.urls.XYURLGenerator;
074import org.jfree.data.xy.XYDataset;
075import org.jfree.util.PublicCloneable;
076import org.jfree.util.ShapeUtilities;
077
078/**
079 * A step chart renderer that fills the area between the step and the x-axis.
080 */
081public class XYStepAreaRenderer extends AbstractXYItemRenderer 
082                                implements XYItemRenderer, 
083                                           Cloneable,
084                                           PublicCloneable,
085                                           Serializable {
086
087    /** For serialization. */
088    private static final long serialVersionUID = -7311560779702649635L;
089    
090    /** Useful constant for specifying the type of rendering (shapes only). */
091    public static final int SHAPES = 1;
092
093    /** Useful constant for specifying the type of rendering (area only). */
094    public static final int AREA = 2;
095
096    /** 
097     * Useful constant for specifying the type of rendering (area and shapes). 
098     */
099    public static final int AREA_AND_SHAPES = 3;
100
101    /** A flag indicating whether or not shapes are drawn at each XY point. */
102    private boolean shapesVisible;
103
104    /** A flag that controls whether or not shapes are filled for ALL series. */
105    private boolean shapesFilled;
106
107    /** A flag indicating whether or not Area are drawn at each XY point. */
108    private boolean plotArea;
109
110    /** A flag that controls whether or not the outline is shown. */
111    private boolean showOutline;
112
113    /** Area of the complete series */
114    protected transient Polygon pArea = null;
115
116    /** 
117     * The value on the range axis which defines the 'lower' border of the 
118     * area. 
119     */
120    private double rangeBase;
121
122    /**
123     * Constructs a new renderer.
124     */
125    public XYStepAreaRenderer() {
126        this(AREA);
127    }
128
129    /**
130     * Constructs a new renderer.
131     *
132     * @param type  the type of the renderer.
133     */
134    public XYStepAreaRenderer(int type) {
135        this(type, null, null);
136    }
137
138    /**
139     * Constructs a new renderer.
140     * <p>
141     * To specify the type of renderer, use one of the constants:
142     * AREA, SHAPES or AREA_AND_SHAPES.
143     *
144     * @param type  the type of renderer.
145     * @param toolTipGenerator  the tool tip generator to use 
146     *                          (<code>null</code> permitted).
147     * @param urlGenerator  the URL generator (<code>null</code> permitted).
148     */
149    public XYStepAreaRenderer(int type,
150                              XYToolTipGenerator toolTipGenerator, 
151                              XYURLGenerator urlGenerator) {
152
153        super();
154        setBaseToolTipGenerator(toolTipGenerator);
155        setURLGenerator(urlGenerator);
156
157        if (type == AREA) {
158            this.plotArea = true;
159        }
160        else if (type == SHAPES) {
161            this.shapesVisible = true;
162        }
163        else if (type == AREA_AND_SHAPES) {
164            this.plotArea = true;
165            this.shapesVisible = true;
166        }
167        this.showOutline = false;
168    }
169
170    /**
171     * Returns a flag that controls whether or not outlines of the areas are 
172     * drawn.
173     *
174     * @return The flag.
175     * 
176     * @see #setOutline(boolean)
177     */
178    public boolean isOutline() {
179        return this.showOutline;
180    }
181
182    /**
183     * Sets a flag that controls whether or not outlines of the areas are 
184     * drawn, and sends a {@link RendererChangeEvent} to all registered 
185     * listeners.
186     *
187     * @param show  the flag.
188     * 
189     * @see #isOutline()
190     */
191    public void setOutline(boolean show) {
192        this.showOutline = show;
193        notifyListeners(new RendererChangeEvent(this));
194    }
195
196    /**
197     * Returns true if shapes are being plotted by the renderer.
198     *
199     * @return <code>true</code> if shapes are being plotted by the renderer.
200     * 
201     * @see #setShapesVisible(boolean)
202     */
203    public boolean getShapesVisible() {
204        return this.shapesVisible;
205    }
206    
207    /**
208     * Sets the flag that controls whether or not shapes are displayed for each 
209     * data item, and sends a {@link RendererChangeEvent} to all registered
210     * listeners.
211     * 
212     * @param flag  the flag.
213     * 
214     * @see #getShapesVisible()
215     */
216    public void setShapesVisible(boolean flag) {
217        this.shapesVisible = flag;
218        notifyListeners(new RendererChangeEvent(this));
219    }
220
221    /**
222     * Returns the flag that controls whether or not the shapes are filled.
223     * 
224     * @return A boolean.
225     * 
226     * @see #setShapesFilled(boolean)
227     */
228    public boolean isShapesFilled() {
229        return this.shapesFilled;
230    }
231    
232    /**
233     * Sets the 'shapes filled' for ALL series.
234     *
235     * @param filled  the flag.
236     * 
237     * @see #isShapesFilled()
238     */
239    public void setShapesFilled(boolean filled) {
240        this.shapesFilled = filled;
241        notifyListeners(new RendererChangeEvent(this));
242    }
243
244    /**
245     * Returns true if Area is being plotted by the renderer.
246     *
247     * @return <code>true</code> if Area is being plotted by the renderer.
248     * 
249     * @see #setPlotArea(boolean)
250     */
251    public boolean getPlotArea() {
252        return this.plotArea;
253    }
254
255    /**
256     * Sets a flag that controls whether or not areas are drawn for each data 
257     * item.
258     * 
259     * @param flag  the flag.
260     * 
261     * @see #getPlotArea()
262     */
263    public void setPlotArea(boolean flag) {
264        this.plotArea = flag;
265        notifyListeners(new RendererChangeEvent(this));
266    }
267    
268    /**
269     * Returns the value on the range axis which defines the 'lower' border of
270     * the area.
271     *
272     * @return <code>double</code> the value on the range axis which defines 
273     *         the 'lower' border of the area.
274     *         
275     * @see #setRangeBase(double)
276     */
277    public double getRangeBase() {
278        return this.rangeBase;
279    }
280
281    /**
282     * Sets the value on the range axis which defines the default border of the 
283     * area.  E.g. setRangeBase(Double.NEGATIVE_INFINITY) lets areas always 
284     * reach the lower border of the plotArea. 
285     * 
286     * @param val  the value on the range axis which defines the default border
287     *             of the area.
288     *             
289     * @see #getRangeBase()
290     */
291    public void setRangeBase(double val) {
292        this.rangeBase = val;
293        notifyListeners(new RendererChangeEvent(this));
294    }
295
296    /**
297     * Initialises the renderer.  Here we calculate the Java2D y-coordinate for
298     * zero, since all the bars have their bases fixed at zero.
299     *
300     * @param g2  the graphics device.
301     * @param dataArea  the area inside the axes.
302     * @param plot  the plot.
303     * @param data  the data.
304     * @param info  an optional info collection object to return data back to 
305     *              the caller.
306     *
307     * @return The number of passes required by the renderer.
308     */
309    public XYItemRendererState initialise(Graphics2D g2,
310                                          Rectangle2D dataArea,
311                                          XYPlot plot,
312                                          XYDataset data,
313                                          PlotRenderingInfo info) {
314
315        
316        XYItemRendererState state = super.initialise(g2, dataArea, plot, data, 
317                info);
318        // disable visible items optimisation - it doesn't work for this
319        // renderer...
320        state.setProcessVisibleItemsOnly(false);
321        return state;
322
323    }
324
325
326    /**
327     * Draws the visual representation of a single data item.
328     *
329     * @param g2  the graphics device.
330     * @param state  the renderer state.
331     * @param dataArea  the area within which the data is being drawn.
332     * @param info  collects information about the drawing.
333     * @param plot  the plot (can be used to obtain standard color information 
334     *              etc).
335     * @param domainAxis  the domain axis.
336     * @param rangeAxis  the range axis.
337     * @param dataset  the dataset.
338     * @param series  the series index (zero-based).
339     * @param item  the item index (zero-based).
340     * @param crosshairState  crosshair information for the plot 
341     *                        (<code>null</code> permitted).
342     * @param pass  the pass index.
343     */
344    public void drawItem(Graphics2D g2,
345                         XYItemRendererState state,
346                         Rectangle2D dataArea,
347                         PlotRenderingInfo info,
348                         XYPlot plot,
349                         ValueAxis domainAxis,
350                         ValueAxis rangeAxis,
351                         XYDataset dataset,
352                         int series,
353                         int item,
354                         CrosshairState crosshairState,
355                         int pass) {
356                             
357        PlotOrientation orientation = plot.getOrientation();
358        
359        // Get the item count for the series, so that we can know which is the 
360        // end of the series.
361        int itemCount = dataset.getItemCount(series);
362
363        Paint paint = getItemPaint(series, item);
364        Stroke seriesStroke = getItemStroke(series, item);
365        g2.setPaint(paint);
366        g2.setStroke(seriesStroke);
367
368        // get the data point...
369        double x1 = dataset.getXValue(series, item);
370        double y1 = dataset.getYValue(series, item);
371        double x = x1;
372        double y = Double.isNaN(y1) ? getRangeBase() : y1;
373        double transX1 = domainAxis.valueToJava2D(x, dataArea, 
374                plot.getDomainAxisEdge());
375        double transY1 = rangeAxis.valueToJava2D(y, dataArea, 
376                plot.getRangeAxisEdge());
377                                                          
378        // avoid possible sun.dc.pr.PRException: endPath: bad path
379        transY1 = restrictValueToDataArea(transY1, plot, dataArea);         
380
381        if (this.pArea == null && !Double.isNaN(y1)) {
382
383            // Create a new Area for the series
384            this.pArea = new Polygon();
385        
386            // start from Y = rangeBase
387            double transY2 = rangeAxis.valueToJava2D(getRangeBase(), dataArea,
388                    plot.getRangeAxisEdge());
389        
390            // avoid possible sun.dc.pr.PRException: endPath: bad path
391            transY2 = restrictValueToDataArea(transY2, plot, dataArea);         
392        
393            // The first point is (x, this.baseYValue)
394            if (orientation == PlotOrientation.VERTICAL) {
395                this.pArea.addPoint((int) transX1, (int) transY2);
396            }
397            else if (orientation == PlotOrientation.HORIZONTAL) {
398                this.pArea.addPoint((int) transY2, (int) transX1);
399            }
400        }
401
402        double transX0 = 0;
403        double transY0 = restrictValueToDataArea(getRangeBase(), plot, 
404                dataArea);
405        
406        double x0;
407        double y0;
408        if (item > 0) {
409            // get the previous data point...
410            x0 = dataset.getXValue(series, item - 1);
411            y0 = Double.isNaN(y1) ? y1 : dataset.getYValue(series, item - 1);
412
413            x = x0;
414            y = Double.isNaN(y0) ? getRangeBase() : y0;
415            transX0 = domainAxis.valueToJava2D(x, dataArea, 
416                    plot.getDomainAxisEdge());
417            transY0 = rangeAxis.valueToJava2D(y, dataArea, 
418                    plot.getRangeAxisEdge());
419
420            // avoid possible sun.dc.pr.PRException: endPath: bad path
421            transY0 = restrictValueToDataArea(transY0, plot, dataArea);
422                        
423            if (Double.isNaN(y1)) {
424                // NULL value -> insert point on base line
425                // instead of 'step point'
426                transX1 = transX0;
427                transY0 = transY1;          
428            }
429            if (transY0 != transY1) {
430                // not just a horizontal bar but need to perform a 'step'.
431                if (orientation == PlotOrientation.VERTICAL) {
432                    this.pArea.addPoint((int) transX1, (int) transY0);
433                }
434                else if (orientation == PlotOrientation.HORIZONTAL) {
435                    this.pArea.addPoint((int) transY0, (int) transX1);
436                }
437            }
438        }           
439
440        Shape shape = null;
441        if (!Double.isNaN(y1)) {
442            // Add each point to Area (x, y)
443            if (orientation == PlotOrientation.VERTICAL) {
444                this.pArea.addPoint((int) transX1, (int) transY1);
445            }
446            else if (orientation == PlotOrientation.HORIZONTAL) {
447                this.pArea.addPoint((int) transY1, (int) transX1);
448            }
449
450            if (getShapesVisible()) {
451                shape = getItemShape(series, item);
452                if (orientation == PlotOrientation.VERTICAL) {
453                    shape = ShapeUtilities.createTranslatedShape(shape, 
454                            transX1, transY1);
455                }
456                else if (orientation == PlotOrientation.HORIZONTAL) {
457                    shape = ShapeUtilities.createTranslatedShape(shape, 
458                            transY1, transX1);
459                }
460                if (isShapesFilled()) {
461                    g2.fill(shape);
462                }   
463                else {
464                    g2.draw(shape);
465                }   
466            }
467            else {
468                if (orientation == PlotOrientation.VERTICAL) {
469                    shape = new Rectangle2D.Double(transX1 - 2, transY1 - 2, 
470                            4.0, 4.0);
471                }
472                else if (orientation == PlotOrientation.HORIZONTAL) {
473                    shape = new Rectangle2D.Double(transY1 - 2, transX1 - 2, 
474                            4.0, 4.0);
475                }
476            }
477        }
478
479        // Check if the item is the last item for the series or if it
480        // is a NULL value and number of items > 0.  We can't draw an area for 
481        // a single point.
482        if (getPlotArea() && item > 0 && this.pArea != null 
483                          && (item == (itemCount - 1) || Double.isNaN(y1))) {
484
485            double transY2 = rangeAxis.valueToJava2D(getRangeBase(), dataArea, 
486                    plot.getRangeAxisEdge());
487
488            // avoid possible sun.dc.pr.PRException: endPath: bad path
489            transY2 = restrictValueToDataArea(transY2, plot, dataArea);         
490
491            if (orientation == PlotOrientation.VERTICAL) {
492                // Add the last point (x,0)
493                this.pArea.addPoint((int) transX1, (int) transY2);
494            }
495            else if (orientation == PlotOrientation.HORIZONTAL) {
496                // Add the last point (x,0)
497                this.pArea.addPoint((int) transY2, (int) transX1);
498            }
499
500            // fill the polygon
501            g2.fill(this.pArea);
502
503            // draw an outline around the Area.
504            if (isOutline()) {
505                g2.setStroke(plot.getOutlineStroke());
506                g2.setPaint(plot.getOutlinePaint());
507                g2.draw(this.pArea);
508            }
509
510            // start new area when needed (see above)
511            this.pArea = null;
512        }
513
514        // do we need to update the crosshair values?
515        if (!Double.isNaN(y1)) {
516            int domainAxisIndex = plot.getDomainAxisIndex(domainAxis);
517            int rangeAxisIndex = plot.getRangeAxisIndex(rangeAxis);
518            updateCrosshairValues(crosshairState, x1, y1, domainAxisIndex, 
519                    rangeAxisIndex, transX1, transY1, orientation);
520        }
521
522        // collect entity and tool tip information...
523        if (state.getInfo() != null) {
524            EntityCollection entities = state.getEntityCollection();
525            if (entities != null && shape != null) {
526                String tip = null;
527                XYToolTipGenerator generator 
528                    = getToolTipGenerator(series, item);
529                if (generator != null) {
530                    tip = generator.generateToolTip(dataset, series, item);
531                }
532                String url = null;
533                if (getURLGenerator() != null) {
534                    url = getURLGenerator().generateURL(dataset, series, item);
535                }
536                XYItemEntity entity = new XYItemEntity(shape, dataset, series, 
537                        item, tip, url);
538                entities.add(entity);
539            }
540        }
541    }
542
543    /**
544     * Tests this renderer for equality with an arbitrary object.
545     * 
546     * @param obj  the object (<code>null</code> permitted).
547     * 
548     * @return A boolean.
549     */
550    public boolean equals(Object obj) {
551        if (obj == this) {    
552            return true;
553        }
554        if (!(obj instanceof XYStepAreaRenderer)) {
555            return false;
556        }
557        XYStepAreaRenderer that = (XYStepAreaRenderer) obj;
558        if (this.showOutline != that.showOutline) {
559            return false;
560        }
561        if (this.shapesVisible != that.shapesVisible) {
562            return false;
563        }
564        if (this.shapesFilled != that.shapesFilled) {
565            return false;
566        }
567        if (this.plotArea != that.plotArea) {
568            return false;
569        }
570        if (this.rangeBase != that.rangeBase) {
571            return false;
572        }
573        return super.equals(obj);
574    }
575    
576    /**
577     * Returns a clone of the renderer.
578     * 
579     * @return A clone.
580     * 
581     * @throws CloneNotSupportedException  if the renderer cannot be cloned.
582     */
583    public Object clone() throws CloneNotSupportedException {
584        return super.clone();
585    }
586    
587    /**
588     * Helper method which returns a value if it lies
589     * inside the visible dataArea and otherwise the corresponding
590     * coordinate on the border of the dataArea. The PlotOrientation
591     * is taken into account. 
592     * Useful to avoid possible sun.dc.pr.PRException: endPath: bad path
593     * which occurs when trying to draw lines/shapes which in large part
594     * lie outside of the visible dataArea.
595     * 
596     * @param value the value which shall be 
597     * @param dataArea  the area within which the data is being drawn.
598     * @param plot  the plot (can be used to obtain standard color 
599     *              information etc).
600     * @return <code>double</code> value inside the data area.
601     */
602    protected static double restrictValueToDataArea(double value, 
603                                                    XYPlot plot, 
604                                                    Rectangle2D dataArea) {
605        double min = 0;
606        double max = 0;
607        if (plot.getOrientation() == PlotOrientation.VERTICAL) {
608            min = dataArea.getMinY();
609            max = dataArea.getMaxY();
610        } 
611        else if (plot.getOrientation() ==  PlotOrientation.HORIZONTAL) {
612            min = dataArea.getMinX();
613            max = dataArea.getMaxX();
614        }       
615        if (value < min) {
616            value = min;
617        }
618        else if (value > max) {
619            value = max;
620        }
621        return value;
622    }
623
624}