    001/* ===========================================================
    002 * Orson Charts : a 3D chart library for the Java(tm) platform
    003 * ===========================================================
    004 *
    005 * (C)opyright 2013-2016, by Object Refinery Limited.  All rights reserved.
    006 *
    007 * http://www.object-refinery.com/orsoncharts/index.html
    008 *
    009 * This program is free software: you can redistribute it and/or modify
    010 * it under the terms of the GNU General Public License as published by
    011 * the Free Software Foundation, either version 3 of the License, or
    012 * (at your option) any later version.
    013 *
    014 * This program is distributed in the hope that it will be useful,
    015 * but WITHOUT ANY WARRANTY; without even the implied warranty of
    016 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    017 * GNU General Public License for more details.
    018 *
    019 * You should have received a copy of the GNU General Public License
    020 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
    021 *
    022 * [Oracle and Java are registered trademarks of Oracle and/or its affiliates.
    023 * Other names may be trademarks of their respective owners.]
    024 *
    025 * If you do not wish to be bound by the terms of the GPL, an alternative
    026 * commercial license can be purchased.  For details, please see visit the
    027 * Orson Charts home page:
    028 *
    029 * http://www.object-refinery.com/orsoncharts/index.html
    030 *
    031 */
    032
    033package com.orsoncharts.renderer.category;
    034
    035import java.awt.Color;
    036import java.io.Serializable;
    037import java.util.ArrayList;
    038import java.util.List;
    039
    040import com.orsoncharts.axis.CategoryAxis3D;
    041import com.orsoncharts.Chart3DFactory;
    042import com.orsoncharts.Range;
    043import com.orsoncharts.axis.ValueAxis3D;
    044import com.orsoncharts.data.category.CategoryDataset3D;
    045import com.orsoncharts.data.DataUtils;
    046import com.orsoncharts.data.Values3D;
    047import com.orsoncharts.data.KeyedValues3DItemKey;
    048import com.orsoncharts.graphics3d.Dimension3D;
    049import com.orsoncharts.graphics3d.Object3D;
    050import com.orsoncharts.graphics3d.Offset3D;
    051import com.orsoncharts.graphics3d.Utils2D;
    052import com.orsoncharts.graphics3d.World;
    053import com.orsoncharts.label.ItemLabelPositioning;
    054import com.orsoncharts.plot.CategoryPlot3D;
    055import com.orsoncharts.renderer.Renderer3DChangeEvent;
    056import com.orsoncharts.util.ObjectUtils;
    057
    058/**
    059 * A renderer for creating 3D area charts from data in a
    060 * {@link CategoryDataset3D} (for use with a {@link CategoryPlot3D}). For
    061 * example:
    062 * <div>
    063 * <object id="ABC" data="../../../../doc-files/AreaChart3DDemo1.svg"
    064 * type="image/svg+xml" width="500" height="359">
    065 * </object>
    066 * </div>
    067 * (refer to {@code AreaChart3DDemo1.java} for the code to generate the
    068 * above chart).
    069 * <br><br>
    070 * There is a factory method to create a chart using this renderer - see
    071 * {@link Chart3DFactory#createAreaChart(String, String, CategoryDataset3D,
    072 * String, String, String)}.
    073 * <br><br>
    074 * NOTE: This class is serializable, but the serialization format is subject
    075 * to change in future releases and should not be relied upon for persisting
    076 * instances of this class.
    077 */
    078@SuppressWarnings("serial")
    079public class AreaRenderer3D extends AbstractCategoryRenderer3D
    080        implements Serializable {
    081
    082    /** The base for the areas (defaults to 0.0). */
    083    private double base;
    084
    085    /**
    086     * The color used to paint the underside of the area object (if
    087     * {@code null}, the regular series color is used).
    088     */
    089    private Color baseColor;
    090
    091    /** The depth of the area. */
    092    private double depth;
    093
    094    /**
    095     * For isolated data values this attribute controls the width (x-axis) of
    096     * the box representing the data item, it is expressed as a percentage of
    097     * the category width.
    098     */
    099    private double isolatedItemWidthPercent;
    100
    101    /**
    102     * The color source that determines the color used to highlight clipped
    103     * items in the chart.
    104     */
    105    private CategoryColorSource clipColorSource;
    106
    107    /**
    108     * A flag that controls whether or not outlines are drawn for the faces
    109     * making up the area segments.
    110     */
    111    private boolean drawFaceOutlines;
    112
    113    /**
    114     * Default constructor.
    115     */
    116    public AreaRenderer3D() {
    117        this.base = 0.0;
    118        this.baseColor = null;
    119        this.depth = 0.6;
    120        this.isolatedItemWidthPercent = 0.25;
    121        this.clipColorSource = new StandardCategoryColorSource(Color.RED);
    122        this.drawFaceOutlines = true;
    123    }
    124
    125    /**
    126     * Returns the y-value for the base of the area.  The default value is
    127     * {@code 0.0}.
    128     *
    129     * @return The base value.
    130     */
    131    public double getBase() {
    132        return this.base;
    133    }
    134
    135    /**
    136     * Sets the base value and sends a change event to all registered listeners.
    137     *
    138     * @param base  the base value.
    139     */
    140    public void setBase(double base) {
    141        this.base = base;
    142        fireChangeEvent(true);
    143    }
    144
    145    /**
    146     * Returns the color used to paint the underside of the area polygons.
    147     * The default value is {@code null} (which means the undersides are
    148     * painted using the regular series color).
    149     *
    150     * @return The color (possibly {@code null}).
    151     *
    152     * @see #setBaseColor(java.awt.Color)
    153     */
    154    public Color getBaseColor() {
    155        return this.baseColor;
    156    }
    157
    158    /**
    159     * Sets the color for the underside of the area shapes and sends a
    160     * change event to all registered listeners.  If you set
    161     * this to {@code null} the base will be painted with the regular
    162     * series color.
    163     *
    164     * @param color  the color ({@code null} permitted).
    165     */
    166    public void setBaseColor(Color color) {
    167        this.baseColor = color;
    168        fireChangeEvent(true);
    169    }
    170
    171    /**
    172     * Returns the depth (in 3D) for the area (in world units).  The
    173     * default value is {@code 0.6}.
    174     *
    175     * @return The depth.
    176     */
    177    public double getDepth() {
    178        return this.depth;
    179    }
    180
    181    /**
    182     * Sets the depth (in 3D) and sends a change event to all registered
    183     * listeners.
    184     *
    185     * @param depth  the depth.
    186     */
    187    public void setDepth(double depth) {
    188        this.depth = depth;
    189        fireChangeEvent(true);
    190    }
    191
    192    /**
    193     * Returns the color source used to determine the color used to highlight
    194     * clipping in the chart elements.  If the source is {@code null},
    195     * then the regular series color is used instead.
    196     *
    197     * @return The color source (possibly {@code null}).
    198     *
    199     * @since 1.3
    200     */
    201    public CategoryColorSource getClipColorSource() {
    202        return this.clipColorSource;
    203    }
    204
    205    /**
    206     * Sets the color source that determines the color used to highlight
    207     * clipping in the chart elements, and sends a {@link Renderer3DChangeEvent}
    208     * to all registered listeners.
    209     *
    210     * @param source  the source ({@code null} permitted).
    211     *
    212     * @since 1.3
    213     */
    214    public void setClipColorSource(CategoryColorSource source) {
    215        this.clipColorSource = source;
    216        fireChangeEvent(true);
    217    }
    218
    219    /**
    220     * Returns the flag that controls whether or not the faces making up area
    221     * segments will be drawn with outlines.  The default value is
    222     * {@code true}.  When anti-aliasing is on, the fill area for the
    223     * faces will have some gray shades around the edges, and these will show
    224     * up on the chart as thin lines (usually not visible if you turn off
    225     * anti-aliasing).  To mask this, the rendering engine can draw an outline
    226     * around each face in the same color (this usually results in cleaner
    227     * output, but it is slower and can introduce some minor visual artifacts
    228     * as well depending on the output target).
    229     *
    230     * @return A boolean.
    231     *
    232     * @since 1.3
    233     */
    234    public boolean getDrawFaceOutlines() {
    235        return this.drawFaceOutlines;
    236    }
    237
    238    /**
    239     * Sets the flag that controls whether or not outlines are drawn for the
    240     * faces making up the area segments and sends a change event to all
    241     * registered listeners.
    242     *
    243     * @param outline  the new flag value.
    244     *
    245     * @since 1.3
    246     */
    247    public void setDrawFaceOutlines(boolean outline) {
    248        this.drawFaceOutlines = outline;
    249        fireChangeEvent(true);
    250    }
    251
    252    /**
    253     * Returns the range (for the value axis) that is required for this
    254     * renderer to show all the values in the specified data set.  This method
    255     * is overridden to ensure that the range includes the base value (normally
    256     * 0.0) set for the renderer.
    257     *
    258     * @param data  the data ({@code null} not permitted).
    259     *
    260     * @return The range.
    261     */
    262    @Override
    263    public Range findValueRange(Values3D<? extends Number> data) {
    264        return DataUtils.findValueRange(data, this.base);
    265    }
    266
    267    /**
    268     * Constructs and places one item from the specified dataset into the given
    269     * world.  This method will be called by the {@link CategoryPlot3D} class
    270     * while iterating over the items in the dataset.
    271     *
    272     * @param dataset  the dataset ({@code null} not permitted).
    273     * @param series  the series index.
    274     * @param row  the row index.
    275     * @param column  the column index.
    276     * @param world  the world ({@code null} not permitted).
    277     * @param dimensions  the plot dimensions ({@code null} not permitted).
    278     * @param xOffset  the x-offset.
    279     * @param yOffset  the y-offset.
    280     * @param zOffset  the z-offset.
    281     */
    282    @Override
    283    public void composeItem(CategoryDataset3D dataset, int series, int row,
    284            int column, World world, Dimension3D dimensions,
    285            double xOffset, double yOffset, double zOffset) {
    286
    287        Number y = dataset.getValue(series, row, column);
    288        Number yprev = null;
    289        if (column > 0) {
    290            yprev = dataset.getValue(series, row, column - 1);
    291        }
    292        Number ynext = null;
    293        if (column < dataset.getColumnCount() - 1) {
    294            ynext = dataset.getValue(series, row, column + 1);
    295        }
    296
    297        CategoryPlot3D plot = getPlot();
    298        CategoryAxis3D rowAxis = plot.getRowAxis();
    299        CategoryAxis3D columnAxis = plot.getColumnAxis();
    300        ValueAxis3D valueAxis = plot.getValueAxis();
    301        Range r = valueAxis.getRange();
    302
    303        Comparable<?> seriesKey = dataset.getSeriesKey(series);
    304        Comparable<?> rowKey = dataset.getRowKey(row);
    305        Comparable<?> columnKey = dataset.getColumnKey(column);
    306        double rowValue = rowAxis.getCategoryValue(rowKey);
    307        double columnValue = columnAxis.getCategoryValue(columnKey);
    308        double ww = dimensions.getWidth();
    309        double hh = dimensions.getHeight();
    310        double dd = dimensions.getDepth();
    311
    312        // for any data value, we'll try to create two area segments, one to
    313        // the left of the value and one to the right of the value (each
    314        // halfway to the adjacent data value).  If the adjacent data values
    315        // are null (or don't exist, as in the case of the first and last data
    316        // items), then we can create an isolated segment to represent the data
    317        // item.  The final consideration is whether the opening and closing
    318        // faces of each segment are filled or not (if the segment connects to
    319        // another segment, there is no need to fill the end face)
    320        boolean createLeftSegment, createRightSegment, createIsolatedSegment;
    321        boolean leftOpen = false;
    322        boolean leftClose = false;
    323        boolean rightOpen = false;
    324        boolean rightClose = false;
    325
    326        // for the first column there is no left segment, we also handle the
    327        // special case where there is just one column of data in which case
    328        // the renderer can only show an isolated data value
    329        if (column == 0) {
    330            createLeftSegment = false;  // never for first item
    331            if (dataset.getColumnCount() == 1) {
    332                createRightSegment = false;
    333                createIsolatedSegment = (y != null);
    334            } else {
    335                createRightSegment = (y != null && ynext != null);
    336                rightOpen = true;
    337                rightClose = false;
    338                createIsolatedSegment = (y != null && ynext == null);
    339            }
    340        }
    341
    342        // for the last column there is no right segment
    343        else if (column == dataset.getColumnCount() - 1) { // last column
    344            createRightSegment = false; // never for the last item
    345            createLeftSegment = (y != null && yprev != null);
    346            leftOpen = false;
    347            leftClose = true;
    348            createIsolatedSegment = (y != null && yprev == null);
    349        }
    350
    351        // for the general case we handle left and right segments or an
    352        // isolated segment if the surrounding data values are null
    353        else {
    354            createLeftSegment = (y != null && yprev != null);
    355            leftOpen = false;
    356            leftClose = (createLeftSegment && ynext == null);
    357            createRightSegment = (y != null && ynext != null);
    358            rightOpen = (createRightSegment && yprev == null);
    359            rightClose = false;
    360            createIsolatedSegment = (y != null
    361                    && yprev == null && ynext == null);
    362        }
    363
    364        // now that we know what we have to create, we'll need some info
    365        // for the construction...world coordinates are required
    366        double xw = columnAxis.translateToWorld(columnValue, ww) + xOffset;
    367        double yw = Double.NaN;
    368        if (y != null) {
    369            yw = valueAxis.translateToWorld(y.doubleValue(), hh) + yOffset;
    370        }
    371        double zw = rowAxis.translateToWorld(rowValue, dd) + zOffset;
    372        double ywmin = valueAxis.translateToWorld(r.getMin(), hh) + yOffset;
    373        double ywmax = valueAxis.translateToWorld(r.getMax(), hh) + yOffset;
    374        double basew = valueAxis.translateToWorld(this.base, hh) + yOffset;
    375        Color color = getColorSource().getColor(series, row, column);
    376        Color clipColor = color;
    377        if (getClipColorSource() != null) {
    378            Color c = getClipColorSource().getColor(series, row, column);
    379            if (c != null) {
    380                clipColor = c;
    381            }
    382        }
    383        KeyedValues3DItemKey itemKey = new KeyedValues3DItemKey(seriesKey,
    384                rowKey, columnKey);
    385
    386        if (createLeftSegment) {
    387            Comparable<?> prevColumnKey = dataset.getColumnKey(column - 1);
    388            double prevColumnValue = columnAxis.getCategoryValue(prevColumnKey);
    389            double prevColumnX = columnAxis.translateToWorld(prevColumnValue,
    390                    ww) + xOffset;
    391            double xl = (prevColumnX + xw) / 2.0;
    392            assert yprev != null;  // we know this because createLeftSegment is
    393                                   // not 'true' otherwise
    394            double yprevw = valueAxis.translateToWorld(yprev.doubleValue(), hh)
    395                    + yOffset;
    396            double yl = (yprevw + yw) / 2.0;
    397            List<Object3D> leftObjs = createSegment(xl, yl, xw, yw, zw,
    398                    basew, ywmin, ywmax, color, this.baseColor, clipColor,
    399                    leftOpen, leftClose);
    400            for (Object3D obj : leftObjs) {
    401                obj.setProperty(Object3D.ITEM_KEY, itemKey);
    402                obj.setOutline(this.drawFaceOutlines);
    403                world.add(obj);
    404            }
    405        }
    406
    407        if (createRightSegment) {
    408            Comparable<?> nextColumnKey = dataset.getColumnKey(column + 1);
    409            double nextColumnValue = columnAxis.getCategoryValue(nextColumnKey);
    410            double nextColumnX = columnAxis.translateToWorld(nextColumnValue,
    411                    ww) + xOffset;
    412            double xr = (nextColumnX + xw) / 2.0;
    413            assert ynext != null; // we know this because createRightSegment is
    414                                  // not 'true' otherwise
    415            double ynextw = valueAxis.translateToWorld(ynext.doubleValue(), hh)
    416                    + yOffset;
    417            double yr = (ynextw + yw) / 2.0;
    418            List<Object3D> rightObjs = createSegment(xw, yw, xr, yr, zw,
    419                    basew, ywmin, ywmax, color, this.baseColor, clipColor,
    420                    rightOpen, rightClose);
    421            for (Object3D obj : rightObjs) {
    422                obj.setProperty(Object3D.ITEM_KEY, itemKey);
    423                obj.setOutline(this.drawFaceOutlines);
    424                world.add(obj);
    425            }
    426        }
    427
    428        if (createIsolatedSegment) {
    429            double cw = columnAxis.getCategoryWidth()
    430                    * this.isolatedItemWidthPercent;
    431            double cww = columnAxis.translateToWorld(cw, ww);
    432            double h = yw - basew;
    433            Object3D isolated = Object3D.createBox(xw, cww, yw - h / 2, h,
    434                    zw, this.depth, color);
    435            isolated.setOutline(this.drawFaceOutlines);
    436            isolated.setProperty(Object3D.ITEM_KEY, itemKey);
    437            world.add(isolated);
    438        }
    439
    440        if (getItemLabelGenerator() != null && !Double.isNaN(yw)
    441                && yw >= ywmin && yw <= ywmax) {
    442            String label = getItemLabelGenerator().generateItemLabel(dataset,
    443                    seriesKey, rowKey, columnKey);
    444            ItemLabelPositioning positioning = getItemLabelPositioning();
    445            Offset3D offsets = getItemLabelOffsets();
    446            double ydelta = dimensions.getHeight() * offsets.getDY();
    447            if (yw < basew) {
    448                ydelta = -ydelta;
    449            }
    450            if (positioning.equals(ItemLabelPositioning.CENTRAL)) {
    451                Object3D labelObj = Object3D.createLabelObject(label,
    452                        getItemLabelFont(), getItemLabelColor(),
    453                        getItemLabelBackgroundColor(), xw, yw + ydelta, zw,
    454                        false, true);
    455
    456                labelObj.setProperty(Object3D.ITEM_KEY, itemKey);
    457                world.add(labelObj);
    458            } else if (positioning.equals(
    459                    ItemLabelPositioning.FRONT_AND_BACK)) {
    460                double zdelta = this.depth / 2 * offsets.getDZ();
    461                Object3D labelObj1 = Object3D.createLabelObject(label,
    462                        getItemLabelFont(), getItemLabelColor(),
    463                        getItemLabelBackgroundColor(), xw, yw + ydelta,
    464                        zw - zdelta, false, false);
    465                labelObj1.setProperty(Object3D.CLASS_KEY, "ItemLabel");
    466                labelObj1.setProperty(Object3D.ITEM_KEY, itemKey);
    467                world.add(labelObj1);
    468                Object3D labelObj2 = Object3D.createLabelObject(label,
    469                        getItemLabelFont(), getItemLabelColor(),
    470                        getItemLabelBackgroundColor(), xw, yw + ydelta,
    471                        zw + zdelta, true, false);
    472                labelObj2.setProperty(Object3D.CLASS_KEY, "ItemLabel");
    473                labelObj2.setProperty(Object3D.ITEM_KEY, itemKey);
    474                world.add(labelObj2);
    475            }
    476        }
    477    }
    478
    479    /**
    480     * Creates objects to represent the area segment between (x0, y0) and
    481     * (x1, y1).
    482     *
    483     * @param x0
    484     * @param y0
    485     * @param x1
    486     * @param y1
    487     * @param z
    488     * @param lineWidth
    489     * @param lineHeight
    490     * @param ymin
    491     * @param ymax
    492     * @param color
    493     * @param clipColor
    494     * @param openingFace
    495     * @param closingFace
    496     *
    497     * @return A list of objects making up the segment.
    498     */
    499    private List<Object3D> createSegment(double x0, double y0, double x1,
    500            double y1, double z, double base, double ymin, double ymax,
    501            Color color, Color baseColor, Color clipColor, boolean openingFace,
    502            boolean closingFace) {
    503
    504        List<Object3D> result = new ArrayList<Object3D>(2);
    505        // either there is a crossing or there is not
    506        if (!isBaselineCrossed(y0, y1, base)) {
    507            Object3D segment = createSegmentWithoutCrossing(x0, y0, x1, y1,
    508                    z, base, ymin, ymax, color, baseColor, clipColor,
    509                    openingFace, closingFace);
    510            result.add(segment);
    511        } else {
    512            result.addAll(createSegmentWithCrossing(x0, y0, x1, y1,
    513                    z, base, ymin, ymax, color, baseColor, clipColor,
    514                    openingFace, closingFace));
    515        }
    516        return result;
    517    }
    518
    519    /**
    520     * Returns {@code true} if the two values are on opposite sides of
    521     * the baseline.  If the data values cross the baseline, then we need
    522     * to construct two 3D objects to represent the data, whereas if there is
    523     * no crossing, a single 3D object will be sufficient.
    524     *
    525     * @param y0  the first value.
    526     * @param y1  the second value.
    527     * @param baseline  the baseline.
    528     *
    529     * @return A boolean.
    530     */
    531    private boolean isBaselineCrossed(double y0, double y1, double baseline) {
    532        return (y0 > baseline && y1 < baseline)
    533                || (y0 < baseline && y1 > baseline);
    534    }
    535
    536    private Object3D createSegmentWithoutCrossing(double x0, double y0,
    537            double x1, double y1, double z, double base, double ymin,
    538            double ymax, Color color, Color baseColor, Color clipColor,
    539            boolean openingFace, boolean closingFace) {
    540
    541        boolean positive = y0 > base || y1 > base;
    542        if (positive) {
    543            Object3D pos = createPositiveArea(x0, y0, x1, y1, base,
    544                    z, new Range(ymin, ymax), color, openingFace,
    545                    closingFace);
    546            return pos;
    547        } else {
    548            Object3D neg = createNegativeArea(x0, y0, x1, y1, base, z,
    549                    new Range(ymin, ymax), color, openingFace, closingFace);
    550            return neg;
    551        }
    552    }
    553
    554    private List<Object3D> createSegmentWithCrossing(double x0, double y0,
    555            double x1, double y1, double z, double base, double ymin,
    556            double ymax, Color color, Color baseColor, Color clipColor,
    557            boolean openingFace, boolean closingFace) {
    558        List<Object3D> result = new ArrayList<Object3D>(2);
    559        Range range = new Range(ymin, ymax);
    560        // find the crossing point
    561        double ydelta = Math.abs(y1 - y0);
    562        double factor = 0;
    563        if (ydelta != 0.0) {
    564            factor = Math.abs(y0 - base) / ydelta;
    565        }
    566        double xcross = x0 + factor * (x1 - x0);
    567        if (y0 > base) {
    568            Object3D pos = createPositiveArea(x0, y0, xcross, base, base, z,
    569                    range, color, openingFace, closingFace);
    570            if (pos != null) {
    571                result.add(pos);
    572            }
    573            Object3D neg = createNegativeArea(xcross, base, x1, y1,
    574                    base, z, range, color, openingFace, closingFace);
    575            if (neg != null) {
    576                result.add(neg);
    577            }
    578        } else {
    579            Object3D neg = createNegativeArea(x0, y0, xcross, base,
    580                    base, z, range, color, openingFace, closingFace);
    581            if (neg != null) {
    582                result.add(neg);
    583            }
    584            Object3D pos = createPositiveArea(xcross, base, x1, y1, base,
    585                    z, range, color, openingFace, closingFace);
    586            if (pos != null) {
    587                result.add(pos);
    588            }
    589        }
    590        return result;
    591    }
    592
    593    /**
    594     * A utility method that returns the fraction (x - x0) / (x1 - x0), which
    595     * is used for some interpolation calculations.
    596     *
    597     * @param x  the x-value.
    598     * @param x0  the start of a range.
    599     * @param x1  the end of a range.
    600     *
    601     * @return The fractional value of x along the range x0 to x1.
    602     */
    603    private double fraction(double x, double x0, double x1) {
    604        double dist = x - x0;
    605        double length = x1 - x0;
    606        return dist / length;
    607    }
    608
    609    /**
    610     * A value in world units that is considered small enough to be not
    611     * significant.  We use this to check if two coordinates are "more or less"
    612     * the same.
    613     */
    614    private static final double EPSILON = 0.001;
    615
    616    /**
    617     * Creates a 3D object to represent a positive "area", taking into account
    618     * that the visible range can be restricted.
    619     *
    620     * @param color  the color ({@code null} not permitted).
    621     * @param wx0
    622     * @param wy0
    623     * @param wx1
    624     * @param wy1
    625     * @param wbase
    626     * @param wz
    627     * @param range
    628     * @param openingFace
    629     * @param closingFace
    630     *
    631     * @return A 3D object or {@code null}.
    632     */
    633    private Object3D createPositiveArea(double wx0, double wy0,
    634            double wx1, double wy1, double wbase, double wz, Range range,
    635            Color color, boolean openingFace, boolean closingFace) {
    636
    637        if (!range.intersects(wy0, wbase) && !range.intersects(wy1, wbase)) {
    638            return null;
    639        }
    640        double wy00 = range.peggedValue(wy0);
    641        double wy11 = range.peggedValue(wy1);
    642        double wbb = range.peggedValue(wbase);
    643
    644        double wx00 = wx0;
    645        if (wy0 < range.getMin()) {
    646            wx00 = wx0 + (wx1 - wx0) * fraction(wy00, wy0, wy1);
    647        }
    648        double wx11 = wx1;
    649        if (wy1 < range.getMin()) {
    650            wx11 = wx1 - (wx1 - wx0) * fraction(wy11, wy1, wy0);
    651        }
    652        double wx22 = Double.NaN;  // bogus
    653        boolean p2required = Utils2D.spans(range.getMax(), wy0, wy1);
    654        if (p2required) {
    655            wx22 = wx0 + (wx1 - wx0) * fraction(range.getMax(), wy0, wy1);
    656        }
    657
    658        double delta = this.depth / 2.0;
    659
    660        // create an area shape
    661        Object3D obj = new Object3D(color, true);
    662        obj.addVertex(wx00, wbb, wz - delta);
    663        obj.addVertex(wx00, wbb, wz + delta);
    664        boolean leftSide = false;
    665        if (Math.abs(wy00 - wbb) > EPSILON) {
    666            leftSide = true;
    667            obj.addVertex(wx00, wy00, wz - delta);
    668            obj.addVertex(wx00, wy00, wz + delta);
    669        }
    670        if (p2required) {
    671            obj.addVertex(wx22, range.getMax(), wz - delta);
    672            obj.addVertex(wx22, range.getMax(), wz + delta);
    673        }
    674        obj.addVertex(wx11, wy11, wz - delta);
    675        obj.addVertex(wx11, wy11, wz + delta);
    676        boolean rightSide = false;
    677        if (Math.abs(wy11 - wbb) > EPSILON) {
    678            rightSide = true;
    679            obj.addVertex(wx11, wbb, wz - delta);
    680            obj.addVertex(wx11, wbb, wz + delta);
    681        }
    682        int vertices = obj.getVertexCount();
    683
    684        if (vertices == 10) {
    685            obj.addFace(new int[] {0, 2, 4, 6, 8});  // front
    686            obj.addFace(new int[] {1, 9, 7, 5, 3});  // rear
    687            obj.addFace(new int[] {0, 8, 9, 1});  // base
    688            obj.addFace(new int[] {2, 3, 5, 4}); // top 1
    689            obj.addFace(new int[] {4, 5, 7, 6});  // top 2
    690            if (openingFace) {
    691                obj.addFace(new int[] {0, 1, 3, 2});
    692            }
    693            if (closingFace) {
    694                obj.addFace(new int[] {6, 7, 9, 8});
    695            }
    696        } else if (vertices == 8) {
    697            obj.addFace(new int[] {0, 2, 4, 6});  // front
    698            obj.addFace(new int[] {7, 5, 3, 1});  // rear
    699            if (!leftSide) {
    700                obj.addFace(new int[] {0, 1, 3, 2});  // top left
    701            }
    702            obj.addFace(new int[] {2, 3, 5, 4});  // top 1
    703            if (!rightSide) {
    704                obj.addFace(new int[] {4, 5, 7, 6}); // top 2
    705            }
    706            obj.addFace(new int[] {1, 0, 6, 7}); // base
    707            if (openingFace) {
    708                obj.addFace(new int[] {0, 1, 3, 2});
    709            }
    710            if (closingFace) {
    711                obj.addFace(new int[] {4, 5, 7, 6});
    712            }
    713        } else if (vertices == 6) {
    714            obj.addFace(new int[] {0, 2, 4}); // front
    715            obj.addFace(new int[] {5, 3, 1}); // rear
    716            if (leftSide) {
    717                obj.addFace(new int[] {3, 5, 4, 2}); // top
    718                if (openingFace) {
    719                    obj.addFace(new int[] {0, 1, 3, 2});
    720                }
    721            } else {
    722                obj.addFace(new int[] {0, 1, 3, 2}); // top
    723                if (closingFace) {
    724                    obj.addFace(new int[] {2, 3, 5, 4});
    725                }
    726            }
    727            obj.addFace(new int[] {0, 4, 5, 1}); // base
    728        } else {
    729            obj.addFace(new int[] {0, 1, 3, 2});
    730            obj.addFace(new int[] {2, 3, 1, 0});
    731        }
    732        return obj;
    733    }
    734
    735    /**
    736     * Creates a negative area shape from (wx0, wy0) to (wx1, wy1) with the
    737     * base at wbase (it is assumed that both wy0 and wy1 are less than wbase).
    738     *
    739     * @param wx0
    740     * @param wy0
    741     * @param wx1
    742     * @param wy1
    743     * @param wbase
    744     * @param wz
    745     * @param range
    746     * @param color
    747     * @param openingFace
    748     * @param closingFace
    749     *
    750     * @return An object representing the area shape (or {@code null}).
    751     */
    752    private Object3D createNegativeArea(double wx0, double wy0,
    753            double wx1, double wy1, double wbase, double wz, Range range,
    754            Color color, boolean openingFace, boolean closingFace) {
    755
    756        if (!range.intersects(wy0, wbase) && !range.intersects(wy1, wbase)) {
    757            return null;
    758        }
    759        double wy00 = range.peggedValue(wy0);
    760        double wy11 = range.peggedValue(wy1);
    761        double wbb = range.peggedValue(wbase);
    762
    763        double wx00 = wx0;
    764        if (wy0 > range.getMax()) {
    765            wx00 = wx0 + (wx1 - wx0) * fraction(wy00, wy0, wy1);
    766        }
    767        double wx11 = wx1;
    768        if (wy1 > range.getMax()) {
    769            wx11 = wx1 - (wx1 - wx0) * fraction(wy11, wy1, wy0);
    770        }
    771        double wx22 = (wx00 + wx11) / 2.0;  // bogus
    772        boolean p2required = Utils2D.spans(range.getMin(), wy0, wy1);
    773        if (p2required) {
    774            wx22 = wx0 + (wx1 - wx0) * fraction(range.getMin(), wy0, wy1);
    775        }
    776
    777        double delta = this.depth / 2.0;
    778
    779        // create an area shape
    780        Object3D obj = new Object3D(color, true);
    781        obj.addVertex(wx00, wbb, wz - delta);
    782        obj.addVertex(wx00, wbb, wz + delta);
    783        boolean leftSide = false;
    784        if (Math.abs(wy00 - wbb) > EPSILON) {
    785            leftSide = true;
    786            obj.addVertex(wx00, wy00, wz - delta);
    787            obj.addVertex(wx00, wy00, wz + delta);
    788        }
    789        if (p2required) {
    790            obj.addVertex(wx22, range.getMin(), wz - delta);
    791            obj.addVertex(wx22, range.getMin(), wz + delta);
    792        }
    793        obj.addVertex(wx11, wy11, wz - delta);
    794        obj.addVertex(wx11, wy11, wz + delta);
    795        boolean rightSide = false;
    796        if (Math.abs(wy11 - wbb) > EPSILON) {
    797            obj.addVertex(wx11, wbb, wz - delta);
    798            obj.addVertex(wx11, wbb, wz + delta);
    799        }
    800        int vertices = obj.getVertexCount();
    801        if (vertices == 10) {
    802            obj.addFace(new int[] {8, 6, 4, 2, 0});  // front
    803            obj.addFace(new int[] {1, 3, 5, 7, 9});  // rear
    804            obj.addFace(new int[] {1, 9, 8, 0});  // base
    805            obj.addFace(new int[] {4, 5, 3, 2}); // top 1
    806            obj.addFace(new int[] {6, 7, 5, 4});  // top 2
    807            if (openingFace) {
    808                obj.addFace(new int[] {2, 3, 1, 0});
    809            }
    810            if (closingFace) {
    811                obj.addFace(new int[] {8, 9, 7, 6});
    812            }
    813        } else if (vertices == 8) {
    814            obj.addFace(new int[] {2, 0, 6, 4});  // front
    815            obj.addFace(new int[] {1, 3, 5, 7});  // rear
    816            obj.addFace(new int[] {0, 1, 7, 6});  // base
    817            if (!leftSide) {
    818                obj.addFace(new int[] {2, 3, 1, 0});
    819            }
    820            obj.addFace(new int[] {3, 2, 4, 5});  // negative top
    821            if (!rightSide) {
    822                obj.addFace(new int[] {6, 7, 5, 4});
    823            }
    824            if (openingFace) {
    825                obj.addFace(new int[] {1, 0, 2, 3});
    826            }
    827            if (closingFace) {
    828                obj.addFace(new int[] {5, 4, 6, 7});
    829            }
    830        } else if (vertices == 6) {
    831            obj.addFace(new int[] {4, 2, 0});  // front
    832            obj.addFace(new int[] {1, 3, 5});  // rear
    833            if (leftSide) {
    834                obj.addFace(new int[] {4, 5, 3, 2});  // negative top
    835                if (openingFace) {
    836                    obj.addFace(new int[] {1, 0, 2, 3});
    837                }
    838            } else {
    839                obj.addFace(new int[] {2, 3, 1, 0}); // negative top
    840                if (closingFace) {
    841                    obj.addFace(new int[] {3, 2, 4, 5});
    842                }
    843            }
    844            obj.addFace(new int[] {0, 1, 5, 4});  // base
    845        } else {
    846            obj.addFace(new int[] {0, 1, 3, 2});
    847            obj.addFace(new int[] {2, 3, 1, 0});
    848        }
    849        return obj;
    850    }
    851
    852    /**
    853     * Tests this renderer for equality with an arbitrary object.
    854     *
    855     * @param obj  the object ({@code null} permitted).
    856     *
    857     * @return A boolean.
    858     */
    859    @Override
    860    public boolean equals(Object obj) {
    861        if (obj == this) {
    862            return true;
    863        }
    864        if (!(obj instanceof AreaRenderer3D)) {
    865            return false;
    866        }
    867        AreaRenderer3D that = (AreaRenderer3D) obj;
    868        if (this.base != that.base) {
    869            return false;
    870        }
    871        if (!ObjectUtils.equals(this.baseColor, that.baseColor)) {
    872            return false;
    873        }
    874        if (this.depth != that.depth) {
    875            return false;
    876        }
    877        return super.equals(obj);
    878    }
    879}