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 * LineRenderer3D.java
029 * -------------------
030 * (C) Copyright 2004-2007, by Tobias Selb and Contributors.
031 *
032 * Original Author:  Tobias Selb (http://www.uepselon.com);
033 * Contributor(s):   David Gilbert (for Object Refinery Limited);
034 *
035 * Changes
036 * -------
037 * 15-Oct-2004 : Version 1 (TS);
038 * 05-Nov-2004 : Modified drawItem() signature (DG);
039 * 11-Nov-2004 : Now uses ShapeUtilities class to translate shapes (DG);
040 * 26-Jan-2005 : Update for changes in super class (DG);
041 * 13-Apr-2005 : Check item visibility in drawItem() method (DG);
042 * 09-Jun-2005 : Use addItemEntity() in drawItem() method (DG);
043 * 10-Jun-2005 : Fixed capitalisation of setXOffset() and setYOffset() (DG);
044 * ------------- JFREECHART 1.0.x ---------------------------------------------
045 * 01-Dec-2006 : Fixed equals() and serialization (DG);
046 * 17-Jan-2007 : Fixed bug in drawDomainGridline() method and added
047 *               argument check to setWallPaint() (DG);
048 * 03-Apr-2007 : Fixed bugs in drawBackground() method (DG);
049 * 16-Oct-2007 : Fixed bug in range marker drawing (DG);
050 * 
051 */
052
053package org.jfree.chart.renderer.category;
054
055import java.awt.AlphaComposite;
056import java.awt.Color;
057import java.awt.Composite;
058import java.awt.Graphics2D;
059import java.awt.Image;
060import java.awt.Paint;
061import java.awt.Shape;
062import java.awt.Stroke;
063import java.awt.geom.GeneralPath;
064import java.awt.geom.Line2D;
065import java.awt.geom.Rectangle2D;
066import java.io.IOException;
067import java.io.ObjectInputStream;
068import java.io.ObjectOutputStream;
069import java.io.Serializable;
070
071import org.jfree.chart.Effect3D;
072import org.jfree.chart.axis.CategoryAxis;
073import org.jfree.chart.axis.ValueAxis;
074import org.jfree.chart.entity.EntityCollection;
075import org.jfree.chart.event.RendererChangeEvent;
076import org.jfree.chart.plot.CategoryPlot;
077import org.jfree.chart.plot.Marker;
078import org.jfree.chart.plot.PlotOrientation;
079import org.jfree.chart.plot.ValueMarker;
080import org.jfree.data.Range;
081import org.jfree.data.category.CategoryDataset;
082import org.jfree.io.SerialUtilities;
083import org.jfree.util.PaintUtilities;
084import org.jfree.util.ShapeUtilities;
085
086/**
087 * A line renderer with a 3D effect.
088 */
089public class LineRenderer3D extends LineAndShapeRenderer 
090                            implements Effect3D, Serializable {
091   
092    /** For serialization. */
093    private static final long serialVersionUID = 5467931468380928736L;
094    
095    /** The default x-offset for the 3D effect. */
096    public static final double DEFAULT_X_OFFSET = 12.0;
097
098    /** The default y-offset for the 3D effect. */
099    public static final double DEFAULT_Y_OFFSET = 8.0;
100   
101    /** The default wall paint. */
102    public static final Paint DEFAULT_WALL_PAINT = new Color(0xDD, 0xDD, 0xDD);
103   
104    /** The size of x-offset for the 3D effect. */
105    private double xOffset;
106
107    /** The size of y-offset for the 3D effect. */
108    private double yOffset;
109   
110    /** The paint used to shade the left and lower 3D wall. */
111    private transient Paint wallPaint;
112   
113    /**
114     * Creates a new renderer.
115     */
116    public LineRenderer3D() {
117        super(true, false);  //Create a line renderer only
118        this.xOffset = DEFAULT_X_OFFSET;
119        this.yOffset = DEFAULT_Y_OFFSET;
120        this.wallPaint = DEFAULT_WALL_PAINT;
121    }
122   
123    /**
124     * Returns the x-offset for the 3D effect.
125     *
126     * @return The x-offset.
127     * 
128     * @see #setXOffset(double)
129     * @see #getYOffset()
130     */
131    public double getXOffset() {
132        return this.xOffset;
133    }
134
135    /**
136     * Returns the y-offset for the 3D effect.
137     *
138     * @return The y-offset.
139     * 
140     * @see #setYOffset(double)
141     * @see #getXOffset()
142     */
143    public double getYOffset() {
144        return this.yOffset;
145    }
146   
147    /**
148     * Sets the x-offset and sends a {@link RendererChangeEvent} to all 
149     * registered listeners.
150     * 
151     * @param xOffset  the x-offset.
152     * 
153     * @see #getXOffset()
154     */
155    public void setXOffset(double xOffset) {
156        this.xOffset = xOffset;
157        notifyListeners(new RendererChangeEvent(this));
158    }
159
160    /**
161     * Sets the y-offset and sends a {@link RendererChangeEvent} to all 
162     * registered listeners.
163     * 
164     * @param yOffset  the y-offset.
165     * 
166     * @see #getYOffset()
167     */
168    public void setYOffset(double yOffset) {
169        this.yOffset = yOffset;
170        notifyListeners(new RendererChangeEvent(this));
171    }
172
173    /**
174     * Returns the paint used to highlight the left and bottom wall in the plot
175     * background.
176     *
177     * @return The paint.
178     * 
179     * @see #setWallPaint(Paint)
180     */
181    public Paint getWallPaint() {
182        return this.wallPaint;
183    }
184
185    /**
186     * Sets the paint used to hightlight the left and bottom walls in the plot
187     * background, and sends a {@link RendererChangeEvent} to all 
188     * registered listeners.
189     *
190     * @param paint  the paint (<code>null</code> not permitted).
191     * 
192     * @see #getWallPaint()
193     */
194    public void setWallPaint(Paint paint) {
195        if (paint == null) {
196            throw new IllegalArgumentException("Null 'paint' argument.");
197        }
198        this.wallPaint = paint;
199        notifyListeners(new RendererChangeEvent(this));
200    }
201   
202    /**
203     * Draws the background for the plot.
204     *
205     * @param g2  the graphics device.
206     * @param plot  the plot.
207     * @param dataArea  the area inside the axes.
208     */
209    public void drawBackground(Graphics2D g2, CategoryPlot plot, 
210                               Rectangle2D dataArea) {
211
212        float x0 = (float) dataArea.getX();
213        float x1 = x0 + (float) Math.abs(this.xOffset);
214        float x3 = (float) dataArea.getMaxX();
215        float x2 = x3 - (float) Math.abs(this.xOffset);
216
217        float y0 = (float) dataArea.getMaxY();
218        float y1 = y0 - (float) Math.abs(this.yOffset);
219        float y3 = (float) dataArea.getMinY();
220        float y2 = y3 + (float) Math.abs(this.yOffset);
221
222        GeneralPath clip = new GeneralPath();
223        clip.moveTo(x0, y0);
224        clip.lineTo(x0, y2);
225        clip.lineTo(x1, y3);
226        clip.lineTo(x3, y3);
227        clip.lineTo(x3, y1);
228        clip.lineTo(x2, y0);
229        clip.closePath();
230
231        Composite originalComposite = g2.getComposite();
232        g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER,
233                plot.getBackgroundAlpha()));
234
235        // fill background...
236        Paint backgroundPaint = plot.getBackgroundPaint();
237        if (backgroundPaint != null) {
238            g2.setPaint(backgroundPaint);
239            g2.fill(clip);
240        }
241
242        GeneralPath leftWall = new GeneralPath();
243        leftWall.moveTo(x0, y0);
244        leftWall.lineTo(x0, y2);
245        leftWall.lineTo(x1, y3);
246        leftWall.lineTo(x1, y1);
247        leftWall.closePath();
248        g2.setPaint(getWallPaint());
249        g2.fill(leftWall);
250
251        GeneralPath bottomWall = new GeneralPath();
252        bottomWall.moveTo(x0, y0);
253        bottomWall.lineTo(x1, y1);
254        bottomWall.lineTo(x3, y1);
255        bottomWall.lineTo(x2, y0);
256        bottomWall.closePath();
257        g2.setPaint(getWallPaint());
258        g2.fill(bottomWall);
259
260        // higlight the background corners...
261        g2.setPaint(Color.lightGray);
262        Line2D corner = new Line2D.Double(x0, y0, x1, y1);
263        g2.draw(corner);
264        corner.setLine(x1, y1, x1, y3);
265        g2.draw(corner);
266        corner.setLine(x1, y1, x3, y1);
267        g2.draw(corner);
268
269        // draw background image, if there is one...
270        Image backgroundImage = plot.getBackgroundImage();
271        if (backgroundImage != null) {
272            Rectangle2D adjusted = new Rectangle2D.Double(dataArea.getX() 
273                    + getXOffset(), dataArea.getY(), 
274                    dataArea.getWidth() - getXOffset(), 
275                    dataArea.getHeight() - getYOffset());
276            plot.drawBackgroundImage(g2, adjusted);
277        }
278        
279        g2.setComposite(originalComposite);
280
281    }
282
283    /**
284     * Draws the outline for the plot.
285     *
286     * @param g2  the graphics device.
287     * @param plot  the plot.
288     * @param dataArea  the area inside the axes.
289     */
290    public void drawOutline(Graphics2D g2, CategoryPlot plot, 
291                            Rectangle2D dataArea) {
292
293        float x0 = (float) dataArea.getX();
294        float x1 = x0 + (float) Math.abs(this.xOffset);
295        float x3 = (float) dataArea.getMaxX();
296        float x2 = x3 - (float) Math.abs(this.xOffset);
297
298        float y0 = (float) dataArea.getMaxY();
299        float y1 = y0 - (float) Math.abs(this.yOffset);
300        float y3 = (float) dataArea.getMinY();
301        float y2 = y3 + (float) Math.abs(this.yOffset);
302
303        GeneralPath clip = new GeneralPath();
304        clip.moveTo(x0, y0);
305        clip.lineTo(x0, y2);
306        clip.lineTo(x1, y3);
307        clip.lineTo(x3, y3);
308        clip.lineTo(x3, y1);
309        clip.lineTo(x2, y0);
310        clip.closePath();
311
312        // put an outline around the data area...
313        Stroke outlineStroke = plot.getOutlineStroke();
314        Paint outlinePaint = plot.getOutlinePaint();
315        if ((outlineStroke != null) && (outlinePaint != null)) {
316            g2.setStroke(outlineStroke);
317            g2.setPaint(outlinePaint);
318            g2.draw(clip);
319        }
320
321    }
322
323    /**
324     * Draws a grid line against the domain axis.
325     *
326     * @param g2  the graphics device.
327     * @param plot  the plot.
328     * @param dataArea  the area for plotting data (not yet adjusted for any 
329     *                  3D effect).
330     * @param value  the Java2D value at which the grid line should be drawn.
331     *
332     */
333    public void drawDomainGridline(Graphics2D g2,
334                                   CategoryPlot plot,
335                                   Rectangle2D dataArea,
336                                   double value) {
337
338        Line2D line1 = null;
339        Line2D line2 = null;
340        PlotOrientation orientation = plot.getOrientation();
341        if (orientation == PlotOrientation.HORIZONTAL) {
342            double y0 = value;
343            double y1 = value - getYOffset();
344            double x0 = dataArea.getMinX();
345            double x1 = x0 + getXOffset();
346            double x2 = dataArea.getMaxX();
347            line1 = new Line2D.Double(x0, y0, x1, y1);
348            line2 = new Line2D.Double(x1, y1, x2, y1);
349        }
350        else if (orientation == PlotOrientation.VERTICAL) {
351            double x0 = value;
352            double x1 = value + getXOffset();
353            double y0 = dataArea.getMaxY();
354            double y1 = y0 - getYOffset();
355            double y2 = dataArea.getMinY();
356            line1 = new Line2D.Double(x0, y0, x1, y1);
357            line2 = new Line2D.Double(x1, y1, x1, y2);
358        }
359        g2.setPaint(plot.getDomainGridlinePaint());
360        g2.setStroke(plot.getDomainGridlineStroke());
361        g2.draw(line1);
362        g2.draw(line2);
363
364    }
365
366    /**
367     * Draws a grid line against the range axis.
368     *
369     * @param g2  the graphics device.
370     * @param plot  the plot.
371     * @param axis  the value axis.
372     * @param dataArea  the area for plotting data (not yet adjusted for any 
373     *                  3D effect).
374     * @param value  the value at which the grid line should be drawn.
375     *
376     */
377    public void drawRangeGridline(Graphics2D g2,
378                                  CategoryPlot plot,
379                                  ValueAxis axis,
380                                  Rectangle2D dataArea,
381                                  double value) {
382
383        Range range = axis.getRange();
384
385        if (!range.contains(value)) {
386            return;
387        }
388
389        Rectangle2D adjusted = new Rectangle2D.Double(dataArea.getX(),
390                dataArea.getY() + getYOffset(),
391                dataArea.getWidth() - getXOffset(),
392                dataArea.getHeight() - getYOffset());
393
394        Line2D line1 = null;
395        Line2D line2 = null;
396        PlotOrientation orientation = plot.getOrientation();
397        if (orientation == PlotOrientation.HORIZONTAL) {
398            double x0 = axis.valueToJava2D(value, adjusted, 
399                    plot.getRangeAxisEdge());
400            double x1 = x0 + getXOffset();
401            double y0 = dataArea.getMaxY();
402            double y1 = y0 - getYOffset();
403            double y2 = dataArea.getMinY();
404            line1 = new Line2D.Double(x0, y0, x1, y1);
405            line2 = new Line2D.Double(x1, y1, x1, y2);
406        }
407        else if (orientation == PlotOrientation.VERTICAL) {
408            double y0 = axis.valueToJava2D(value, adjusted,
409                    plot.getRangeAxisEdge());
410            double y1 = y0 - getYOffset();
411            double x0 = dataArea.getMinX();
412            double x1 = x0 + getXOffset();
413            double x2 = dataArea.getMaxX();
414            line1 = new Line2D.Double(x0, y0, x1, y1);
415            line2 = new Line2D.Double(x1, y1, x2, y1);
416        }
417        g2.setPaint(plot.getRangeGridlinePaint());
418        g2.setStroke(plot.getRangeGridlineStroke());
419        g2.draw(line1);
420        g2.draw(line2);
421
422    }
423
424    /**
425     * Draws a range marker.
426     *
427     * @param g2  the graphics device.
428     * @param plot  the plot.
429     * @param axis  the value axis.
430     * @param marker  the marker.
431     * @param dataArea  the area for plotting data (not including 3D effect).
432     */
433    public void drawRangeMarker(Graphics2D g2,
434                                CategoryPlot plot,
435                                ValueAxis axis,
436                                Marker marker,
437                                Rectangle2D dataArea) {
438
439        Rectangle2D adjusted = new Rectangle2D.Double(dataArea.getX(), 
440                dataArea.getY() + getYOffset(), 
441                dataArea.getWidth() - getXOffset(), 
442                dataArea.getHeight() - getYOffset());
443
444        if (marker instanceof ValueMarker) {
445            ValueMarker vm = (ValueMarker) marker;
446            double value = vm.getValue();
447            Range range = axis.getRange();
448            if (!range.contains(value)) {
449                return;
450            }
451
452            GeneralPath path = null;
453            PlotOrientation orientation = plot.getOrientation();
454            if (orientation == PlotOrientation.HORIZONTAL) {
455                float x = (float) axis.valueToJava2D(value, adjusted, 
456                        plot.getRangeAxisEdge());
457                float y = (float) adjusted.getMaxY();
458                path = new GeneralPath();
459                path.moveTo(x, y);
460                path.lineTo((float) (x + getXOffset()), 
461                        y - (float) getYOffset());
462                path.lineTo((float) (x + getXOffset()), 
463                        (float) (adjusted.getMinY() - getYOffset()));
464                path.lineTo(x, (float) adjusted.getMinY());
465                path.closePath();
466            }
467            else if (orientation == PlotOrientation.VERTICAL) {
468                float y = (float) axis.valueToJava2D(value, adjusted, 
469                        plot.getRangeAxisEdge());
470                float x = (float) dataArea.getX();
471                path = new GeneralPath();
472                path.moveTo(x, y);
473                path.lineTo(x + (float) this.xOffset, y - (float) this.yOffset);
474                path.lineTo((float) (adjusted.getMaxX() + this.xOffset), 
475                        y - (float) this.yOffset);
476                path.lineTo((float) (adjusted.getMaxX()), y);
477                path.closePath();
478            }
479            g2.setPaint(marker.getPaint());
480            g2.fill(path);
481            g2.setPaint(marker.getOutlinePaint());
482            g2.draw(path);
483        }
484        else {
485            super.drawRangeMarker(g2, plot, axis, marker, adjusted);
486            // TODO: draw the interval marker with a 3D effect
487        }
488    }
489   
490   /**
491     * Draw a single data item.
492     *
493     * @param g2  the graphics device.
494     * @param state  the renderer state.
495     * @param dataArea  the area in which the data is drawn.
496     * @param plot  the plot.
497     * @param domainAxis  the domain axis.
498     * @param rangeAxis  the range axis.
499     * @param dataset  the dataset.
500     * @param row  the row index (zero-based).
501     * @param column  the column index (zero-based).
502     * @param pass  the pass index.
503     */
504    public void drawItem(Graphics2D g2,
505                         CategoryItemRendererState state,
506                         Rectangle2D dataArea,
507                         CategoryPlot plot,
508                         CategoryAxis domainAxis,
509                         ValueAxis rangeAxis,
510                         CategoryDataset dataset,
511                         int row,
512                         int column,
513                         int pass) {
514
515        if (!getItemVisible(row, column)) {
516            return;   
517        }
518        
519        // nothing is drawn for null...
520        Number v = dataset.getValue(row, column);
521        if (v == null) {
522            return;
523        }
524       
525        Rectangle2D adjusted = new Rectangle2D.Double(dataArea.getX(),
526                dataArea.getY() + getYOffset(), 
527                dataArea.getWidth() - getXOffset(),
528                dataArea.getHeight() - getYOffset());
529       
530        PlotOrientation orientation = plot.getOrientation();
531
532        // current data point...
533        double x1 = domainAxis.getCategoryMiddle(column, getColumnCount(), 
534                adjusted, plot.getDomainAxisEdge());
535        double value = v.doubleValue();
536        double y1 = rangeAxis.valueToJava2D(value, adjusted, 
537                plot.getRangeAxisEdge());
538
539        Shape shape = getItemShape(row, column);
540        if (orientation == PlotOrientation.HORIZONTAL) {
541            shape = ShapeUtilities.createTranslatedShape(shape, y1, x1);
542        }
543        else if (orientation == PlotOrientation.VERTICAL) {
544            shape = ShapeUtilities.createTranslatedShape(shape, x1, y1);
545        }
546       
547        if (getItemLineVisible(row, column)) {
548            if (column != 0) {
549
550                Number previousValue = dataset.getValue(row, column - 1);
551                if (previousValue != null) {
552
553                    // previous data point...
554                    double previous = previousValue.doubleValue();
555                    double x0 = domainAxis.getCategoryMiddle(column - 1, 
556                            getColumnCount(), adjusted, 
557                            plot.getDomainAxisEdge());
558                    double y0 = rangeAxis.valueToJava2D(previous, adjusted, 
559                            plot.getRangeAxisEdge());
560
561                    double x2 = x0 + getXOffset();
562                    double y2 = y0 - getYOffset();
563                    double x3 = x1 + getXOffset();
564                    double y3 = y1 - getYOffset();
565                   
566                    GeneralPath clip = new GeneralPath();
567                   
568                    if (orientation == PlotOrientation.HORIZONTAL) {
569                        clip.moveTo((float) y0, (float) x0);
570                        clip.lineTo((float) y1, (float) x1);
571                        clip.lineTo((float) y3, (float) x3);
572                        clip.lineTo((float) y2, (float) x2);
573                        clip.lineTo((float) y0, (float) x0);
574                        clip.closePath();
575                    }
576                    else if (orientation == PlotOrientation.VERTICAL) {
577                        clip.moveTo((float) x0, (float) y0);
578                        clip.lineTo((float) x1, (float) y1);
579                        clip.lineTo((float) x3, (float) y3);
580                        clip.lineTo((float) x2, (float) y2);
581                        clip.lineTo((float) x0, (float) y0);
582                        clip.closePath();
583                    }
584                   
585                    g2.setPaint(getItemPaint(row, column));
586                    g2.fill(clip);
587                    g2.setStroke(getItemOutlineStroke(row, column));
588                    g2.setPaint(getItemOutlinePaint(row, column));
589                    g2.draw(clip);
590                }
591            }
592        }
593
594        // draw the item label if there is one...
595        if (isItemLabelVisible(row, column)) {
596            drawItemLabel(g2, orientation, dataset, row, column, x1, y1, 
597                    (value < 0.0));
598        }
599
600        // add an item entity, if this information is being collected
601        EntityCollection entities = state.getEntityCollection();
602        if (entities != null) {
603            addItemEntity(entities, dataset, row, column, shape);
604        }
605
606    }
607    
608    /**
609     * Checks this renderer for equality with an arbitrary object.
610     * 
611     * @param obj  the object (<code>null</code> permitted).
612     * 
613     * @return A boolean.
614     */
615    public boolean equals(Object obj) {
616        if (obj == this) {
617            return true;
618        }
619        if (!(obj instanceof LineRenderer3D)) {
620            return false;
621        }
622        LineRenderer3D that = (LineRenderer3D) obj;
623        if (this.xOffset != that.xOffset) {
624            return false;
625        }
626        if (this.yOffset != that.yOffset) {
627            return false;
628        }
629        if (!PaintUtilities.equal(this.wallPaint, that.wallPaint)) {
630            return false;
631        }
632        return super.equals(obj);
633    }
634    
635    /**
636     * Provides serialization support.
637     *
638     * @param stream  the output stream.
639     *
640     * @throws IOException  if there is an I/O error.
641     */
642    private void writeObject(ObjectOutputStream stream) throws IOException {
643        stream.defaultWriteObject();
644        SerialUtilities.writePaint(this.wallPaint, stream);
645    }
646
647    /**
648     * Provides serialization support.
649     *
650     * @param stream  the input stream.
651     *
652     * @throws IOException  if there is an I/O error.
653     * @throws ClassNotFoundException  if there is a classpath problem.
654     */
655    private void readObject(ObjectInputStream stream)
656            throws IOException, ClassNotFoundException {
657        stream.defaultReadObject();
658        this.wallPaint = SerialUtilities.readPaint(stream);
659    }
660
661}