    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}