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