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}