/* ===========================================================
* Orson Charts : a 3D chart library for the Java(tm) platform
* ===========================================================
*
* (C)opyright 2013-2016, by Object Refinery Limited. All rights reserved.
*
* http://www.object-refinery.com/orsoncharts/index.html
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* [Oracle and Java are registered trademarks of Oracle and/or its affiliates.
* Other names may be trademarks of their respective owners.]
*
* If you do not wish to be bound by the terms of the GPL, an alternative
* commercial license can be purchased. For details, please see visit the
* Orson Charts home page:
*
* http://www.object-refinery.com/orsoncharts/index.html
*
*/
package com.orsoncharts.renderer.xyz;
import java.awt.Color;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import com.orsoncharts.Range;
import com.orsoncharts.axis.ValueAxis3D;
import com.orsoncharts.data.function.Function3D;
import com.orsoncharts.data.function.Function3DUtils;
import com.orsoncharts.data.xyz.XYZDataset;
import com.orsoncharts.graphics3d.Dimension3D;
import com.orsoncharts.graphics3d.Object3D;
import com.orsoncharts.graphics3d.Point3D;
import com.orsoncharts.graphics3d.World;
import com.orsoncharts.plot.XYZPlot;
import com.orsoncharts.renderer.ColorScale;
import com.orsoncharts.renderer.ColorScaleRenderer;
import com.orsoncharts.renderer.ComposeType;
import com.orsoncharts.renderer.FixedColorScale;
import com.orsoncharts.renderer.Renderer3DChangeEvent;
import com.orsoncharts.util.ArgChecks;
/**
* A renderer that plots a surface based on a function (any implementation
* of {@link Function3D}). This renderer is different to others in that it
* does not plot data from a dataset, instead it samples a function and plots
* those values. By default 900 samples are taken (30 x-values by 30 z-values)
* although this can be modified.
* <br><br>
* For the fastest rendering, the {@code drawFaceOutlines} flag can be set
* to {@code false} (the default is {@code true}) but this may
* cause slight rendering artifacts if anti-aliasing is on (note that switching
* off anti-aliasing as well also improves rendering performance).
* <br><br>
* NOTE: This class is serializable, but the serialization format is subject
* to change in future releases and should not be relied upon for persisting
* instances of this class.
*
* @since 1.1
*/
@SuppressWarnings("serial")
public class SurfaceRenderer extends AbstractXYZRenderer implements XYZRenderer,
ColorScaleRenderer, Serializable {
/** The function. */
private Function3D function;
/** The number of samples along the x-axis (minimum 2). */
private int xSamples;
/** The number of samples along the z-axis (minimum 2). */
private int zSamples;
/** The color scale. */
private ColorScale colorScale;
/**
* A flag that controls whether the faces that make up the surface have
* their outlines drawn (in addition to the shape being filled). The
* default value is {@code true} which renders a solid surface but
* is slower.
*/
private boolean drawFaceOutlines;
/**
* Creates a new renderer for the specified function. By default, the
* renderer will take 30 samples along the x-axis and 30 samples along the
* z-axis (this is configurable).
*
* @param function the function ({@code null} not permitted).
*/
public SurfaceRenderer(Function3D function) {
ArgChecks.nullNotPermitted(function, "function");
this.function = function;
this.xSamples = 30;
this.zSamples = 30;
this.colorScale = new FixedColorScale(Color.YELLOW);
this.drawFaceOutlines = true;
}
/**
* Returns the number of samples the renderer will take along the
* x-axis when plotting the function. The default value is 30.
*
* @return The number of samples.
*/
public int getXSamples() {
return this.xSamples;
}
/**
* Sets the number of samples the renderer will take along the x-axis when
* plotting the function and sends a {@link Renderer3DChangeEvent} to all
* registered listeners. The default value is 30, setting this to higher
* values will result in smoother looking plots, but they will take
* longer to draw.
*
* @param count the count.
*
* @see #setZSamples(int)
*/
public void setXSamples(int count) {
this.xSamples = count;
fireChangeEvent(true);
}
/**
* Returns the number of samples the renderer will take along the z-axis
* when plotting the function and sends a {@link Renderer3DChangeEvent} to
* all registered listeners. The default value is 30.
*
* @return The number of samples.
*/
public int getZSamples() {
return this.zSamples;
}
/**
* Sets the number of samples the renderer will take along the z-axis when
* plotting the function and sends a {@link Renderer3DChangeEvent} to all
* registered listeners. The default value is 30, setting this to higher
* values will result in smoother looking plots, but they will take
* longer to draw.
*
* @param count the count.
*
* @see #setXSamples(int)
*/
public void setZSamples(int count) {
this.zSamples = count;
}
/**
* Returns the compose-type for the renderer. Here the value is
* {@code ComposeType.ALL} which means the plot will call the
* {@link #composeAll(com.orsoncharts.plot.XYZPlot,
* com.orsoncharts.graphics3d.World, com.orsoncharts.graphics3d.Dimension3D,
* double, double, double)} method for composing the chart.
*
* @return The compose type (never {@code null}).
*/
@Override
public ComposeType getComposeType() {
return ComposeType.ALL;
}
/**
* Returns the color scale. This determines the color of the surface
* according to the y-value.
*
* @return The color scale (never {@code null}).
*/
@Override
public ColorScale getColorScale() {
return this.colorScale;
}
/**
* Sets the color scale and sends a {@link Renderer3DChangeEvent} to all
* registered listeners.
*
* @param colorScale the color scale ({@code null} not permitted).
*/
public void setColorScale(ColorScale colorScale) {
ArgChecks.nullNotPermitted(colorScale, "colorScale");
this.colorScale = colorScale;
fireChangeEvent(true);
}
/**
* Returns the flag that controls whether or not the faces that make
* up the surface have their outlines drawn during rendering. The
* default value is {@code true}.
*
* @return A boolean.
*/
public boolean getDrawFaceOutlines() {
return this.drawFaceOutlines;
}
/**
* Sets a flag that controls whether or not the faces that make up the
* surface are drawn (as well as filled) and sends a
* {@link Renderer3DChangeEvent} to all registered listeners. If the face
* outlines are drawn (the default), the surface is solid (but takes longer
* to draw). If the face outlines are not drawn, Java2D can leave small
* gaps that you can "see" through (if you don't mind this, then the
* performance is better).
*
* @param draw the new flag value.
*/
public void setDrawFaceOutlines(boolean draw) {
this.drawFaceOutlines = draw;
fireChangeEvent(true);
}
/**
* Composes the entire representation of the function in the supplied
* {@code world}.
*
* @param plot the plot.
* @param world the world.
* @param dimensions the plot dimensions.
* @param xOffset the x-offset.
* @param yOffset the y-offset.
* @param zOffset the z-offset.
*/
@Override
public void composeAll(XYZPlot plot, World world, Dimension3D dimensions,
double xOffset, double yOffset, double zOffset) {
// need to know the x-axis range and the z-axis range
ValueAxis3D xAxis = plot.getXAxis();
ValueAxis3D yAxis = plot.getYAxis();
ValueAxis3D zAxis = plot.getZAxis();
Dimension3D dim = plot.getDimensions();
double xlen = dim.getWidth();
double ylen = dim.getHeight();
double zlen = dim.getDepth();
Range yRange = new Range(yOffset, -yOffset);
for (int xIndex = 0; xIndex < this.xSamples; xIndex++) {
double xfrac0 = xIndex / (double) this.xSamples;
double xfrac1 = (xIndex + 1) / (double) this.xSamples;
for (int zIndex = 0; zIndex < this.zSamples; zIndex++) {
double zfrac0 = zIndex / (double) this.zSamples;
double zfrac1 = (zIndex + 1) / (double) this.zSamples;
double x0 = xAxis.getRange().value(xfrac0);
double x1 = xAxis.getRange().value(xfrac1);
double xm = x0 / 2.0 + x1 / 2.0;
double z0 = zAxis.getRange().value(zfrac0);
double z1 = zAxis.getRange().value(zfrac1);
double zm = z0 / 2.0 + z1 / 2.0;
double y00 = this.function.getValue(x0, z0);
double y01 = this.function.getValue(x0, z1);
double y10 = this.function.getValue(x1, z0);
double y11 = this.function.getValue(x1, z1);
double ymm = this.function.getValue(xm, zm);
double wx0 = xAxis.translateToWorld(x0, xlen) + xOffset;
double wx1 = xAxis.translateToWorld(x1, xlen) + xOffset;
double wy00 = yAxis.translateToWorld(y00, ylen) + yOffset;
double wy01 = yAxis.translateToWorld(y01, ylen) + yOffset;
double wy10 = yAxis.translateToWorld(y10, ylen) + yOffset;
double wy11 = yAxis.translateToWorld(y11, ylen) + yOffset;
double wz0 = zAxis.translateToWorld(z0, zlen) + zOffset;
double wz1 = zAxis.translateToWorld(z1, zlen) + zOffset;
Color color = this.colorScale.valueToColor(ymm);
Object3D obj = new Object3D(color, this.drawFaceOutlines);
List<Point3D> pts1 = facePoints1(wx0, wx1, wz0, wz1, wy00, wy01,
wy11, yRange);
int count1 = pts1.size();
for (Point3D pt : pts1) {
obj.addVertex(pt);
}
if (count1 == 3) {
obj.addDoubleSidedFace(new int[] {0, 1, 2});
} else if (count1 == 4) {
obj.addDoubleSidedFace(new int[] {0, 1, 2, 3});
} else if (count1 == 5) {
obj.addDoubleSidedFace(new int[] {0, 1, 2, 3, 4});
}
List<Point3D> pts2 = facePoints2(wx0, wx1, wz0, wz1, wy00, wy11,
wy10, yRange);
int count2 = pts2.size();
for (Point3D pt : pts2) {
obj.addVertex(pt);
}
if (count2 == 3) {
obj.addDoubleSidedFace(new int[] {count1, count1 + 1,
count1 + 2});
} else if (count2 == 4) {
obj.addDoubleSidedFace(new int[] {count1, count1 + 1,
count1 + 2, count1 + 3});
} else if (count2 == 5) {
obj.addDoubleSidedFace(new int[] {count1, count1 + 1,
count1 + 2, count1 + 3, count1 + 4});
}
world.add(obj);
}
}
}
private Point3D intersectPoint(double x0, double y0, double z0, double x1,
double y1, double z1, double yy) {
double p = (yy - y0) / (y1 - y0);
double x = x0 + p * (x1 - x0);
double y = y0 + p * (y1 - y0);
double z = z0 + p * (z1 - z0);
return new Point3D(x, y, z);
}
private List<Point3D> facePoints1(double x0, double x1, double z0,
double z1, double y00, double y01, double y11, Range yRange) {
List<Point3D> pts = new ArrayList<Point3D>(4);
double ymin = yRange.getMin();
double ymax = yRange.getMax();
// handle y00
if (y00 > yRange.getMax()) {
if (yRange.contains(y01)) {
pts.add(intersectPoint(x0, y00, z0, x0, y01, z1, ymax));
} else if (y01 < yRange.getMin()) {
pts.add(intersectPoint(x0, y00, z0, x0, y01, z1, ymax));
pts.add(intersectPoint(x0, y00, z0, x0, y01, z1, ymin));
}
} else if (yRange.contains(y00)) {
pts.add(new Point3D(x0, y00, z0));
if (y01 > yRange.getMax()) {
pts.add(intersectPoint(x0, y00, z0, x0, y01, z1, ymax));
} else if (y01 < yRange.getMin()) {
pts.add(intersectPoint(x0, y00, z0, x0, y01, z1, ymin));
}
} else { // below the range
if (yRange.contains(y01)) {
pts.add(intersectPoint(x0, y00, z0, x0, y01, z1, ymin));
} else if (y01 > yRange.getMax()) {
pts.add(intersectPoint(x0, y00, z0, x0, y01, z1, ymin));
pts.add(intersectPoint(x0, y00, z0, x0, y01, z1, ymax));
}
}
// handle y01
if (y01 > yRange.getMax()) {
if (yRange.contains(y11)) {
pts.add(intersectPoint(x0, y01, z1, x1, y11, z1, ymax));
} else if (y11 < yRange.getMin()) {
pts.add(intersectPoint(x0, y01, z1, x1, y11, z1, ymax));
pts.add(intersectPoint(x0, y01, z1, x1, y11, z1, ymin));
}
} else if (yRange.contains(y01)) {
pts.add(new Point3D(x0, y01, z1));
if (y11 > yRange.getMax()) {
pts.add(intersectPoint(x0, y01, z1, x1, y11, z1, ymax));
} else if (y11 < yRange.getMin()) {
pts.add(intersectPoint(x0, y01, z1, x1, y11, z1, ymin));
}
} else {
if (y11 > yRange.getMax()) {
pts.add(intersectPoint(x0, y01, z1, x1, y11, z1, ymin));
pts.add(intersectPoint(x0, y01, z1, x1, y11, z1, ymax));
} else if (yRange.contains(y11)) {
pts.add(intersectPoint(x0, y01, z1, x1, y11, z1, ymin));
}
}
// handle y11
if (y11 > yRange.getMax()) {
if (yRange.contains(y00)) {
pts.add(intersectPoint(x1, y11, z1, x0, y00, z0, ymax));
} else if (y00 < yRange.getMin()) {
pts.add(intersectPoint(x1, y11, z1, x0, y00, z0, ymax));
pts.add(intersectPoint(x1, y11, z1, x0, y00, z0, ymin));
}
} else if (yRange.contains(y11)) {
pts.add(new Point3D(x1, y11, z1));
if (y00 > yRange.getMax()) {
pts.add(intersectPoint(x1, y11, z1, x0, y00, z0, ymax));
} else if (y00 < yRange.getMin()) {
pts.add(intersectPoint(x1, y11, z1, x0, y00, z0, ymin));
}
} else {
if (y00 > yRange.getMax()) {
pts.add(intersectPoint(x1, y11, z1, x0, y00, z0, ymin));
pts.add(intersectPoint(x1, y11, z1, x0, y00, z0, ymax));
} else if (yRange.contains(y00)) {
pts.add(intersectPoint(x1, y11, z1, x0, y00, z0, ymin));
}
}
return pts;
}
private List<Point3D> facePoints2(double x0, double x1, double z0,
double z1, double y00, double y11, double y10, Range yRange) {
List<Point3D> pts = new ArrayList<Point3D>(4);
double ymin = yRange.getMin();
double ymax = yRange.getMax();
// handle y00
if (y00 > yRange.getMax()) {
if (yRange.contains(y11)) {
pts.add(intersectPoint(x0, y00, z0, x1, y11, z1, ymax));
} else if (y11 < yRange.getMin()) {
pts.add(intersectPoint(x0, y00, z0, x1, y11, z1, ymax));
pts.add(intersectPoint(x0, y00, z0, x1, y11, z1, ymin));
}
} else if (yRange.contains(y00)) {
pts.add(new Point3D(x0, y00, z0));
if (y11 > yRange.getMax()) {
pts.add(intersectPoint(x0, y00, z0, x1, y11, z1, ymax));
} else if (y11 < yRange.getMin()) {
pts.add(intersectPoint(x0, y00, z0, x1, y11, z1, ymin));
}
} else { // below the range
if (yRange.contains(y11)) {
pts.add(intersectPoint(x0, y00, z0, x1, y11, z1, ymin));
} else if (y11 > yRange.getMax()) {
pts.add(intersectPoint(x0, y00, z0, x1, y11, z1, ymin));
pts.add(intersectPoint(x0, y00, z0, x1, y11, z1, ymax));
}
}
// handle y11
if (y11 > yRange.getMax()) {
if (yRange.contains(y10)) {
pts.add(intersectPoint(x1, y11, z1, x1, y10, z0, ymax));
} else if (y10 < yRange.getMin()) {
pts.add(intersectPoint(x1, y11, z1, x1, y10, z0, ymax));
pts.add(intersectPoint(x1, y11, z1, x1, y10, z0, ymin));
}
} else if (yRange.contains(y11)) {
pts.add(new Point3D(x1, y11, z1));
if (y10 > yRange.getMax()) {
pts.add(intersectPoint(x1, y11, z1, x1, y10, z0, ymax));
} else if (y10 < yRange.getMin()) {
pts.add(intersectPoint(x1, y11, z1, x1, y10, z0, ymin));
}
} else {
if (y10 > yRange.getMax()) {
pts.add(intersectPoint(x1, y11, z1, x1, y10, z0, ymin));
pts.add(intersectPoint(x1, y11, z1, x1, y10, z0, ymax));
} else if (yRange.contains(y10)) {
pts.add(intersectPoint(x1, y11, z1, x1, y10, z0, ymin));
}
}
// handle y10
if (y10 > yRange.getMax()) {
if (yRange.contains(y00)) {
pts.add(intersectPoint(x1, y10, z0, x0, y00, z0, ymax));
} else if (y00 < yRange.getMin()) {
pts.add(intersectPoint(x1, y10, z0, x0, y00, z0, ymax));
pts.add(intersectPoint(x1, y10, z0, x0, y00, z0, ymin));
}
} else if (yRange.contains(y10)) {
pts.add(new Point3D(x1, y10, z0));
if (y00 > yRange.getMax()) {
pts.add(intersectPoint(x1, y10, z0, x0, y00, z0, ymax));
} else if (y00 < yRange.getMin()) {
pts.add(intersectPoint(x1, y10, z0, x0, y00, z0, ymin));
}
} else {
if (y00 > yRange.getMax()) {
pts.add(intersectPoint(x1, y10, z0, x0, y00, z0, ymin));
pts.add(intersectPoint(x1, y10, z0, x0, y00, z0, ymax));
} else if (yRange.contains(y00)) {
pts.add(intersectPoint(x1, y10, z0, x0, y00, z0, ymin));
}
}
return pts;
}
/**
* Throws an {@code UnsupportedOperationException} because this
* renderer does not support per-item rendering.
*
* @param dataset the dataset ({@code null} not permitted).
* @param series the series index.
* @param item the item index.
* @param world the world ({@code null} not permitted).
* @param dimensions the dimensions ({@code null} not permitted).
* @param xOffset the x-offset.
* @param yOffset the y-offset.
* @param zOffset the z-offset.
*/
@Override
public void composeItem(XYZDataset dataset, int series, int item,
World world, Dimension3D dimensions, double xOffset,
double yOffset, double zOffset) {
throw new UnsupportedOperationException(
"Not supported by this renderer.");
}
/**
* Returns the current range for the x-axis - the method is overridden
* because this renderer does not use a dataset (it samples and plots a
* function directly).
*
* @param dataset the dataset (ignored).
*
* @return The x-range (never {@code null}).
*/
@Override
public Range findXRange(XYZDataset dataset) {
return getPlot().getXAxis().getRange();
}
/**
* Returns the range that the renderer requires on the y-axis to display
* all the data in the function.
*
* @param dataset the dataset (ignored).
*
* @return The range.
*/
@Override
public Range findYRange(XYZDataset dataset) {
return Function3DUtils.findYRange(this.function,
getPlot().getXAxis().getRange(),
getPlot().getZAxis().getRange(),
this.xSamples, this.zSamples, true);
}
/**
* Returns the current range for the z-axis - the method is overridden
* because this renderer does not use a dataset (it samples and plots a
* function directly).
*
* @param dataset the dataset (ignored).
*
* @return The z-range (never {@code null}).
*/
@Override
public Range findZRange(XYZDataset dataset) {
return getPlot().getZAxis().getRange();
}
/**
* Tests this renderer for equality with an arbitrary object.
*
* @param obj the object ({@code null} not permitted).
*
* @return A boolean.
*/
@Override
public boolean equals(Object obj) {
if (obj == this) {
return true;
}
if (!(obj instanceof SurfaceRenderer)) {
return false;
}
SurfaceRenderer that = (SurfaceRenderer) obj;
if (!this.function.equals(that.function)) {
return false;
}
if (this.xSamples != that.xSamples) {
return false;
}
if (this.zSamples != that.zSamples) {
return false;
}
if (!this.colorScale.equals(that.colorScale)) {
return false;
}
if (this.drawFaceOutlines != that.drawFaceOutlines) {
return false;
}
return super.equals(obj);
}
}