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.xyz;
034
035import java.awt.Color;
036import java.io.Serializable;
037import java.util.ArrayList;
038import java.util.List;
039
040import org.jfree.chart3d.axis.ValueAxis3D;
041import org.jfree.chart3d.data.Range;
042import org.jfree.chart3d.data.function.Function3D;
043import org.jfree.chart3d.data.function.Function3DUtils;
044import org.jfree.chart3d.data.xyz.XYZDataset;
045import org.jfree.chart3d.graphics3d.Dimension3D;
046import org.jfree.chart3d.graphics3d.Object3D;
047import org.jfree.chart3d.graphics3d.Point3D;
048import org.jfree.chart3d.graphics3d.World;
049import org.jfree.chart3d.internal.Args;
050import org.jfree.chart3d.plot.XYZPlot;
051import org.jfree.chart3d.renderer.ColorScale;
052import org.jfree.chart3d.renderer.ColorScaleRenderer;
053import org.jfree.chart3d.renderer.ComposeType;
054import org.jfree.chart3d.renderer.FixedColorScale;
055import org.jfree.chart3d.renderer.Renderer3DChangeEvent;
056
057/**
058 * A renderer that plots a surface based on a function (any implementation
059 * of {@link Function3D}).  This renderer is different to others in that it
060 * does not plot data from a dataset, instead it samples a function and plots
061 * those values.  By default 900 samples are taken (30 x-values by 30 z-values)
062 * although this can be modified.
063 * <br><br>
064 * For the fastest rendering, the {@code drawFaceOutlines} flag can be set 
065 * to {@code false} (the default is {@code true}) but this may 
066 * cause slight rendering artifacts if anti-aliasing is on (note that switching
067 * off anti-aliasing as well also improves rendering performance).
068 * <br><br>
069 * NOTE: This class is serializable, but the serialization format is subject 
070 * to change in future releases and should not be relied upon for persisting 
071 * instances of this class.
072 * 
073 * @since 1.1
074 */
075@SuppressWarnings("serial")
076public class SurfaceRenderer extends AbstractXYZRenderer implements XYZRenderer,
077        ColorScaleRenderer, Serializable {
078    
079    /** The function. */
080    private Function3D function;
081
082    /** The number of samples along the x-axis (minimum 2). */
083    private int xSamples;
084    
085    /** The number of samples along the z-axis (minimum 2). */
086    private int zSamples;
087    
088    /** The color scale. */
089    private ColorScale colorScale;
090    
091    /** 
092     * A flag that controls whether the faces that make up the surface have
093     * their outlines drawn (in addition to the shape being filled).  The
094     * default value is {@code true} which renders a solid surface but
095     * is slower.
096     */
097    private boolean drawFaceOutlines;
098    
099    /**
100     * Creates a new renderer for the specified function.  By default, the 
101     * renderer will take 30 samples along the x-axis and 30 samples along the 
102     * z-axis (this is configurable).
103     * 
104     * @param function  the function ({@code null} not permitted). 
105     */
106    public SurfaceRenderer(Function3D function) {
107        Args.nullNotPermitted(function, "function");
108        this.function = function;
109        this.xSamples = 30;
110        this.zSamples = 30;
111        this.colorScale = new FixedColorScale(Color.YELLOW);
112        this.drawFaceOutlines = true;
113    }
114    
115    /**
116     * Returns the number of samples the renderer will take along the
117     * x-axis when plotting the function.  The default value is 30.
118     * 
119     * @return The number of samples. 
120     */
121    public int getXSamples() {
122        return this.xSamples;
123    }
124    
125    /**
126     * Sets the number of samples the renderer will take along the x-axis when
127     * plotting the function and sends a {@link Renderer3DChangeEvent} to all
128     * registered listeners.  The default value is 30, setting this to higher
129     * values will result in smoother looking plots, but they will take
130     * longer to draw.
131     * 
132     * @param count  the count. 
133     * 
134     * @see #setZSamples(int) 
135     */
136    public void setXSamples(int count) {
137        this.xSamples = count;
138        fireChangeEvent(true);
139    }
140    
141    /**
142     * Returns the number of samples the renderer will take along the z-axis
143     * when plotting the function and sends a {@link Renderer3DChangeEvent} to
144     * all registered listeners.  The default value is 30.
145     * 
146     * @return The number of samples.
147     */
148    public int getZSamples() {
149        return this.zSamples;
150    }
151    
152    /**
153     * Sets the number of samples the renderer will take along the z-axis when
154     * plotting the function and sends a {@link Renderer3DChangeEvent} to all
155     * registered listeners.  The default value is 30, setting this to higher
156     * values will result in smoother looking plots, but they will take
157     * longer to draw.
158     * 
159     * @param count  the count. 
160     * 
161     * @see #setXSamples(int) 
162     */
163    public void setZSamples(int count) {
164        this.zSamples = count;
165    }
166    
167    /**
168     * Returns the compose-type for the renderer.  Here the value is
169     * {@code ComposeType.ALL} which means the plot will call the 
170     * {@link #composeAll(org.jfree.chart3d.plot.XYZPlot, 
171     * org.jfree.chart3d.graphics3d.World, org.jfree.chart3d.graphics3d.Dimension3D,
172     * double, double, double)} method for composing the chart.
173     * 
174     * @return The compose type (never {@code null}). 
175     */
176    @Override
177    public ComposeType getComposeType() {
178        return ComposeType.ALL;
179    }
180    
181    /**
182     * Returns the color scale.  This determines the color of the surface
183     * according to the y-value.
184     * 
185     * @return The color scale (never {@code null}). 
186     */
187    @Override
188    public ColorScale getColorScale() {
189        return this.colorScale;
190    }
191    
192    /**
193     * Sets the color scale and sends a {@link Renderer3DChangeEvent} to all 
194     * registered listeners.
195     * 
196     * @param colorScale  the color scale ({@code null} not permitted). 
197     */
198    public void setColorScale(ColorScale colorScale) {
199        Args.nullNotPermitted(colorScale, "colorScale");
200        this.colorScale = colorScale;
201        fireChangeEvent(true);
202    }
203    
204    /**
205     * Returns the flag that controls whether or not the faces that make
206     * up the surface have their outlines drawn during rendering.  The
207     * default value is {@code true}.  
208     * 
209     * @return A boolean. 
210     */
211    public boolean getDrawFaceOutlines() {
212        return this.drawFaceOutlines;
213    }
214    
215    /**
216     * Sets a flag that controls whether or not the faces that make up the 
217     * surface are drawn (as well as filled) and sends a 
218     * {@link Renderer3DChangeEvent} to all registered listeners.  If the face 
219     * outlines are drawn (the default), the surface is solid (but takes longer
220     * to draw).  If the face outlines are not drawn, Java2D can leave small 
221     * gaps that you can "see" through (if you don't mind this, then the
222     * performance is better).
223     * 
224     * @param draw  the new flag value. 
225     */
226    public void setDrawFaceOutlines(boolean draw) {
227        this.drawFaceOutlines = draw;
228        fireChangeEvent(true);
229    }
230    
231    /**
232     * Composes the entire representation of the function in the supplied
233     * {@code world}.
234     * 
235     * @param plot  the plot.
236     * @param world  the world.
237     * @param dimensions  the plot dimensions.
238     * @param xOffset  the x-offset.
239     * @param yOffset  the y-offset.
240     * @param zOffset  the z-offset.
241     */
242    @Override
243    public void composeAll(XYZPlot plot, World world, Dimension3D dimensions, 
244            double xOffset, double yOffset, double zOffset) {
245        
246        // need to know the x-axis range and the z-axis range
247        ValueAxis3D xAxis = plot.getXAxis();
248        ValueAxis3D yAxis = plot.getYAxis();
249        ValueAxis3D zAxis = plot.getZAxis();
250        Dimension3D dim = plot.getDimensions();
251        double xlen = dim.getWidth();
252        double ylen = dim.getHeight();
253        double zlen = dim.getDepth();
254        Range yRange = new Range(yOffset, -yOffset);
255        for (int xIndex = 0; xIndex < this.xSamples; xIndex++) {
256            double xfrac0 = xIndex / (double) this.xSamples;
257            double xfrac1 = (xIndex + 1) / (double) this.xSamples;
258            for (int zIndex = 0; zIndex < this.zSamples; zIndex++) {
259                double zfrac0 = zIndex / (double) this.zSamples;
260                double zfrac1 = (zIndex + 1) / (double) this.zSamples;
261                
262                double x0 = xAxis.getRange().value(xfrac0);
263                double x1 = xAxis.getRange().value(xfrac1);
264                double xm = x0 / 2.0 + x1 / 2.0;
265                double z0 = zAxis.getRange().value(zfrac0);
266                double z1 = zAxis.getRange().value(zfrac1);
267                double zm = z0 / 2.0 + z1 / 2.0;
268                double y00 = this.function.getValue(x0, z0);
269                double y01 = this.function.getValue(x0, z1);
270                double y10 = this.function.getValue(x1, z0);
271                double y11 = this.function.getValue(x1, z1);
272                double ymm = this.function.getValue(xm, zm);
273                
274                double wx0 = xAxis.translateToWorld(x0, xlen) + xOffset;
275                double wx1 = xAxis.translateToWorld(x1, xlen) + xOffset;
276                double wy00 = yAxis.translateToWorld(y00, ylen) + yOffset;
277                double wy01 = yAxis.translateToWorld(y01, ylen) + yOffset;
278                double wy10 = yAxis.translateToWorld(y10, ylen) + yOffset;
279                double wy11 = yAxis.translateToWorld(y11, ylen) + yOffset;
280                double wz0 = zAxis.translateToWorld(z0, zlen) + zOffset;
281                double wz1 = zAxis.translateToWorld(z1, zlen) + zOffset;
282
283                Color color = this.colorScale.valueToColor(ymm);
284                Object3D obj = new Object3D(color, this.drawFaceOutlines);
285                List<Point3D> pts1 = facePoints1(wx0, wx1, wz0, wz1, wy00, wy01, 
286                        wy11, yRange);
287                int count1 = pts1.size();
288                for (Point3D pt : pts1) {
289                    obj.addVertex(pt);
290                }
291                if (count1 == 3) {
292                    obj.addDoubleSidedFace(new int[] {0, 1, 2});
293                } else if (count1 == 4) {
294                    obj.addDoubleSidedFace(new int[] {0, 1, 2, 3});
295                } else if (count1 == 5) {
296                    obj.addDoubleSidedFace(new int[] {0, 1, 2, 3, 4});
297                }
298                List<Point3D> pts2 = facePoints2(wx0, wx1, wz0, wz1, wy00, wy11,
299                        wy10, yRange);
300                int count2 = pts2.size();
301                for (Point3D pt : pts2) {
302                    obj.addVertex(pt);
303                }
304                if (count2 == 3) {
305                    obj.addDoubleSidedFace(new int[] {count1, count1 + 1, 
306                        count1 + 2});
307                } else if (count2 == 4) {
308                    obj.addDoubleSidedFace(new int[] {count1, count1 + 1, 
309                        count1 + 2, count1 + 3});
310                } else if (count2 == 5) {
311                    obj.addDoubleSidedFace(new int[] {count1, count1 + 1, 
312                        count1 + 2, count1 + 3, count1 + 4});                    
313                }
314                world.add(obj);
315            }
316            
317        }        
318    }
319    
320    private Point3D intersectPoint(double x0, double y0, double z0, double x1, 
321                double y1, double z1, double yy) {
322        double p = (yy - y0) / (y1 - y0);
323        double x = x0 + p * (x1 - x0);
324        double y = y0 + p * (y1 - y0);
325        double z = z0 + p * (z1 - z0);
326        return new Point3D(x, y, z);
327    }
328    
329    private List<Point3D> facePoints1(double x0, double x1, double z0, 
330            double z1, double y00, double y01, double y11, Range yRange) {
331        
332        List<Point3D> pts = new ArrayList<>(4);
333        double ymin = yRange.getMin();
334        double ymax = yRange.getMax();
335        
336        // handle y00
337        if (y00 > yRange.getMax()) {
338            if (yRange.contains(y01)) {
339                pts.add(intersectPoint(x0, y00, z0, x0, y01, z1, ymax));
340            } else if (y01 < yRange.getMin()) {
341                pts.add(intersectPoint(x0, y00, z0, x0, y01, z1, ymax));
342                pts.add(intersectPoint(x0, y00, z0, x0, y01, z1, ymin));
343            }
344        } else if (yRange.contains(y00)) {
345            pts.add(new Point3D(x0, y00, z0));
346            if (y01 > yRange.getMax()) {
347                pts.add(intersectPoint(x0, y00, z0, x0, y01, z1, ymax));
348            } else if (y01 < yRange.getMin()) {
349                pts.add(intersectPoint(x0, y00, z0, x0, y01, z1, ymin));
350            }
351        } else { // below the range
352            if (yRange.contains(y01)) {
353                pts.add(intersectPoint(x0, y00, z0, x0, y01, z1, ymin));
354            } else if (y01 > yRange.getMax()) {
355                pts.add(intersectPoint(x0, y00, z0, x0, y01, z1, ymin));
356                pts.add(intersectPoint(x0, y00, z0, x0, y01, z1, ymax));
357            }
358        }
359        
360        // handle y01
361        if (y01 > yRange.getMax()) {
362            if (yRange.contains(y11)) {
363                pts.add(intersectPoint(x0, y01, z1, x1, y11, z1, ymax));
364            } else if (y11 < yRange.getMin()) {
365                pts.add(intersectPoint(x0, y01, z1, x1, y11, z1, ymax));
366                pts.add(intersectPoint(x0, y01, z1, x1, y11, z1, ymin));
367            }
368        } else if (yRange.contains(y01)) {
369            pts.add(new Point3D(x0, y01, z1));
370            if (y11 > yRange.getMax()) {
371                pts.add(intersectPoint(x0, y01, z1, x1, y11, z1, ymax));                
372            } else if (y11 < yRange.getMin()) {
373                pts.add(intersectPoint(x0, y01, z1, x1, y11, z1, ymin));         
374            }
375        } else {
376            if (y11 > yRange.getMax()) {
377                pts.add(intersectPoint(x0, y01, z1, x1, y11, z1, ymin));
378                pts.add(intersectPoint(x0, y01, z1, x1, y11, z1, ymax));
379            } else if (yRange.contains(y11)) {
380                pts.add(intersectPoint(x0, y01, z1, x1, y11, z1, ymin));                
381            }
382        }
383        
384        // handle y11
385        if (y11 > yRange.getMax()) {
386            if (yRange.contains(y00)) {
387                pts.add(intersectPoint(x1, y11, z1, x0, y00, z0, ymax));                
388            } else if (y00 < yRange.getMin()) {
389                pts.add(intersectPoint(x1, y11, z1, x0, y00, z0, ymax));                
390                pts.add(intersectPoint(x1, y11, z1, x0, y00, z0, ymin));
391            }
392        } else if (yRange.contains(y11)) {
393            pts.add(new Point3D(x1, y11, z1));
394            if (y00 > yRange.getMax()) {
395                pts.add(intersectPoint(x1, y11, z1, x0, y00, z0, ymax));                
396            } else if (y00 < yRange.getMin()) {
397                pts.add(intersectPoint(x1, y11, z1, x0, y00, z0, ymin));                
398            }
399        } else {
400            if (y00 > yRange.getMax()) {
401                pts.add(intersectPoint(x1, y11, z1, x0, y00, z0, ymin));                
402                pts.add(intersectPoint(x1, y11, z1, x0, y00, z0, ymax));                
403            } else if (yRange.contains(y00)) {
404                pts.add(intersectPoint(x1, y11, z1, x0, y00, z0, ymin));                
405            }
406        }
407        return pts;
408    }
409    
410    private List<Point3D> facePoints2(double x0, double x1, double z0, 
411            double z1, double y00, double y11, double y10, Range yRange) {
412        
413        List<Point3D> pts = new ArrayList<>(4);
414        double ymin = yRange.getMin();
415        double ymax = yRange.getMax();        
416        // handle y00
417        if (y00 > yRange.getMax()) {
418            if (yRange.contains(y11)) {
419                pts.add(intersectPoint(x0, y00, z0, x1, y11, z1, ymax));
420            } else if (y11 < yRange.getMin()) {
421                pts.add(intersectPoint(x0, y00, z0, x1, y11, z1, ymax));
422                pts.add(intersectPoint(x0, y00, z0, x1, y11, z1, ymin));
423            }
424        } else if (yRange.contains(y00)) {
425            pts.add(new Point3D(x0, y00, z0));
426            if (y11 > yRange.getMax()) {
427                pts.add(intersectPoint(x0, y00, z0, x1, y11, z1, ymax));
428            } else if (y11 < yRange.getMin()) {
429                pts.add(intersectPoint(x0, y00, z0, x1, y11, z1, ymin));
430            }
431        } else { // below the range
432            if (yRange.contains(y11)) {
433                pts.add(intersectPoint(x0, y00, z0, x1, y11, z1, ymin));
434            } else if (y11 > yRange.getMax()) {
435                pts.add(intersectPoint(x0, y00, z0, x1, y11, z1, ymin));
436                pts.add(intersectPoint(x0, y00, z0, x1, y11, z1, ymax));
437            }
438        }
439        
440        // handle y11
441        if (y11 > yRange.getMax()) {
442            if (yRange.contains(y10)) {
443                pts.add(intersectPoint(x1, y11, z1, x1, y10, z0, ymax));
444            } else if (y10 < yRange.getMin()) {
445                pts.add(intersectPoint(x1, y11, z1, x1, y10, z0, ymax));
446                pts.add(intersectPoint(x1, y11, z1, x1, y10, z0, ymin));
447            }
448        } else if (yRange.contains(y11)) {
449            pts.add(new Point3D(x1, y11, z1));
450            if (y10 > yRange.getMax()) {
451                pts.add(intersectPoint(x1, y11, z1, x1, y10, z0, ymax));                
452            } else if (y10 < yRange.getMin()) {
453                pts.add(intersectPoint(x1, y11, z1, x1, y10, z0, ymin));         
454            }
455        } else {
456            if (y10 > yRange.getMax()) {
457                pts.add(intersectPoint(x1, y11, z1, x1, y10, z0, ymin));
458                pts.add(intersectPoint(x1, y11, z1, x1, y10, z0, ymax));
459            } else if (yRange.contains(y10)) {
460                pts.add(intersectPoint(x1, y11, z1, x1, y10, z0, ymin));                
461            }
462        }
463        
464        // handle y10
465        if (y10 > yRange.getMax()) {
466            if (yRange.contains(y00)) {
467                pts.add(intersectPoint(x1, y10, z0, x0, y00, z0, ymax));                
468            } else if (y00 < yRange.getMin()) {
469                pts.add(intersectPoint(x1, y10, z0, x0, y00, z0, ymax));                
470                pts.add(intersectPoint(x1, y10, z0, x0, y00, z0, ymin));
471            }
472        } else if (yRange.contains(y10)) {
473            pts.add(new Point3D(x1, y10, z0));
474            if (y00 > yRange.getMax()) {
475                pts.add(intersectPoint(x1, y10, z0, x0, y00, z0, ymax));                
476            } else if (y00 < yRange.getMin()) {
477                pts.add(intersectPoint(x1, y10, z0, x0, y00, z0, ymin));                
478            }
479        } else {
480            if (y00 > yRange.getMax()) {
481                pts.add(intersectPoint(x1, y10, z0, x0, y00, z0, ymin));                
482                pts.add(intersectPoint(x1, y10, z0, x0, y00, z0, ymax));                
483            } else if (yRange.contains(y00)) {
484                pts.add(intersectPoint(x1, y10, z0, x0, y00, z0, ymin));                
485            }
486        }
487        
488        return pts;
489    }
490
491    /**
492     * Throws an {@code UnsupportedOperationException} because this 
493     * renderer does not support per-item rendering.
494     * 
495     * @param dataset the dataset ({@code null} not permitted).
496     * @param series  the series index.
497     * @param item  the item index.
498     * @param world  the world ({@code null} not permitted).
499     * @param dimensions  the dimensions ({@code null} not permitted).
500     * @param xOffset  the x-offset.
501     * @param yOffset  the y-offset.
502     * @param zOffset  the z-offset.
503     */
504    @Override
505    public void composeItem(XYZDataset dataset, int series, int item, 
506            World world, Dimension3D dimensions, double xOffset, 
507            double yOffset, double zOffset) {
508        throw new UnsupportedOperationException(
509                "Not supported by this renderer.");
510    }
511    
512    /**
513     * Returns the current range for the x-axis - the method is overridden 
514     * because this renderer does not use a dataset (it samples and plots a
515     * function directly).
516     * 
517     * @param dataset  the dataset (ignored).
518     * 
519     * @return The x-range (never {@code null}). 
520     */
521    @Override
522    public Range findXRange(XYZDataset dataset) {
523        return getPlot().getXAxis().getRange();
524    }
525
526    /**
527     * Returns the range that the renderer requires on the y-axis to display
528     * all the data in the function.
529     * 
530     * @param dataset  the dataset (ignored).
531     * 
532     * @return The range. 
533     */
534    @Override
535    public Range findYRange(XYZDataset dataset) {
536        return Function3DUtils.findYRange(this.function, 
537                getPlot().getXAxis().getRange(), 
538                getPlot().getZAxis().getRange(), 
539                this.xSamples, this.zSamples, true);
540    }
541
542    /**
543     * Returns the current range for the z-axis - the method is overridden 
544     * because this renderer does not use a dataset (it samples and plots a
545     * function directly).
546     * 
547     * @param dataset  the dataset (ignored).
548     * 
549     * @return The z-range (never {@code null}). 
550     */
551    @Override
552    public Range findZRange(XYZDataset dataset) {
553        return getPlot().getZAxis().getRange();
554    }
555
556    /**
557     * Tests this renderer for equality with an arbitrary object.
558     * 
559     * @param obj  the object ({@code null} not permitted).
560     * 
561     * @return A boolean. 
562     */
563    @Override
564    public boolean equals(Object obj) {
565        if (obj == this) {
566            return true;
567        }
568        if (!(obj instanceof SurfaceRenderer)) {
569            return false;
570        }
571        SurfaceRenderer that = (SurfaceRenderer) obj;
572        if (!this.function.equals(that.function)) {
573            return false;
574        }
575        if (this.xSamples != that.xSamples) {
576            return false;
577        }
578        if (this.zSamples != that.zSamples) {
579            return false;
580        }
581        if (!this.colorScale.equals(that.colorScale)) {
582            return false;
583        }
584        if (this.drawFaceOutlines != that.drawFaceOutlines) {
585            return false;
586        }
587        return super.equals(obj);
588    }
589}