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