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 @SuppressWarnings("unchecked")
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 = (Number) dataset.getValue(series, row, column);
288        Number yprev = null;
289        if (column > 0) {
290            yprev = (Number) dataset.getValue(series, row, column - 1);
291        }
292        Number ynext = null;
293        if (column < dataset.getColumnCount() - 1) {
294            ynext = (Number) 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}