    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
    056    package org.jfree.chart.renderer.category;
    057
    058    import java.awt.AlphaComposite;
    059    import java.awt.Color;
    060    import java.awt.Composite;
    061    import java.awt.Graphics2D;
    062    import java.awt.Image;
    063    import java.awt.Paint;
    064    import java.awt.Shape;
    065    import java.awt.Stroke;
    066    import java.awt.geom.GeneralPath;
    067    import java.awt.geom.Line2D;
    068    import java.awt.geom.Rectangle2D;
    069    import java.io.IOException;
    070    import java.io.ObjectInputStream;
    071    import java.io.ObjectOutputStream;
    072    import java.io.Serializable;
    073
    074    import org.jfree.chart.Effect3D;
    075    import org.jfree.chart.axis.CategoryAxis;
    076    import org.jfree.chart.axis.ValueAxis;
    077    import org.jfree.chart.entity.EntityCollection;
    078    import org.jfree.chart.event.RendererChangeEvent;
    079    import org.jfree.chart.plot.CategoryPlot;
    080    import org.jfree.chart.plot.Marker;
    081    import org.jfree.chart.plot.PlotOrientation;
    082    import org.jfree.chart.plot.ValueMarker;
    083    import org.jfree.data.Range;
    084    import org.jfree.data.category.CategoryDataset;
    085    import org.jfree.io.SerialUtilities;
    086    import org.jfree.util.PaintUtilities;
    087    import 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     */
    097    public 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    }