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