/* ===========================================================
* 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.plot;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Paint;
import java.awt.Stroke;
import java.io.Serializable;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.ArrayList;
import java.util.List;
import com.orsoncharts.ChartElementVisitor;
import com.orsoncharts.axis.Axis3DChangeEvent;
import com.orsoncharts.axis.Axis3DChangeListener;
import com.orsoncharts.axis.ValueAxis3D;
import com.orsoncharts.data.Dataset3DChangeEvent;
import com.orsoncharts.data.Dataset3DChangeListener;
import com.orsoncharts.data.ItemKey;
import com.orsoncharts.data.xyz.XYZDataset;
import com.orsoncharts.data.xyz.XYZItemKey;
import com.orsoncharts.renderer.xyz.XYZRenderer;
import com.orsoncharts.util.ArgChecks;
import com.orsoncharts.graphics3d.Dimension3D;
import com.orsoncharts.graphics3d.World;
import com.orsoncharts.label.StandardXYZLabelGenerator;
import com.orsoncharts.label.StandardXYZItemLabelGenerator;
import com.orsoncharts.label.XYZLabelGenerator;
import com.orsoncharts.label.XYZItemLabelGenerator;
import com.orsoncharts.legend.LegendItemInfo;
import com.orsoncharts.legend.StandardLegendItemInfo;
import com.orsoncharts.renderer.ComposeType;
import com.orsoncharts.renderer.Renderer3DChangeEvent;
import com.orsoncharts.renderer.Renderer3DChangeListener;
import com.orsoncharts.util.ObjectUtils;
import com.orsoncharts.util.SerialUtils;
/**
* A 3D plot with three numerical axes that displays data from an
* {@link XYZDataset}.
* <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.
*/
@SuppressWarnings("serial")
public class XYZPlot extends AbstractPlot3D implements Dataset3DChangeListener,
Axis3DChangeListener, Renderer3DChangeListener, Serializable {
private static Stroke DEFAULT_GRIDLINE_STROKE = new BasicStroke(0.5f,
BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND, 1f,
new float[] { 3f, 3f }, 0f);
/** The dataset. */
private XYZDataset dataset;
/** The renderer. */
private XYZRenderer renderer;
/** The x-axis. */
private ValueAxis3D xAxis;
/** The y-axis. */
private ValueAxis3D yAxis;
/** The z-axis. */
private ValueAxis3D zAxis;
/** Are gridlines visible for the x-axis? */
private boolean gridlinesVisibleX;
/** The paint for the x-axis gridlines. */
private transient Paint gridlinePaintX;
/** The stroke for the x-axis gridlines. */
private transient Stroke gridlineStrokeX;
/** Are gridlines visible for the y-axis? */
private boolean gridlinesVisibleY;
/** The paint for the y-axis gridlines. */
private transient Paint gridlinePaintY;
/** The stroke for the y-axis gridlines. */
private transient Stroke gridlineStrokeY;
/** Are gridlines visible for the z-axis? */
private boolean gridlinesVisibleZ;
/** The paint for the z-axis gridlines. */
private transient Paint gridlinePaintZ;
/** The stroke for the z-axis gridlines. */
private transient Stroke gridlineStrokeZ;
/** The legend label generator. */
private XYZLabelGenerator legendLabelGenerator;
/** The tool tip generator (if null there will be no tooltips). */
private XYZItemLabelGenerator toolTipGenerator;
/**
* Creates a new plot with the specified axes.
*
* @param dataset the dataset ({@code null} not permitted).
* @param renderer the renderer ({@code null} not permitted).
* @param xAxis the x-axis ({@code null} not permitted).
* @param yAxis the y-axis ({@code null} not permitted).
* @param zAxis the z-axis ({@code null} not permitted).
*/
public XYZPlot(XYZDataset dataset, XYZRenderer renderer, ValueAxis3D xAxis,
ValueAxis3D yAxis, ValueAxis3D zAxis) {
ArgChecks.nullNotPermitted(dataset, "dataset");
ArgChecks.nullNotPermitted(renderer, "renderer");
ArgChecks.nullNotPermitted(xAxis, "xAxis");
ArgChecks.nullNotPermitted(yAxis, "yAxis");
ArgChecks.nullNotPermitted(zAxis, "zAxis");
this.dimensions = new Dimension3D(10, 10, 10);
this.dataset = dataset;
this.dataset.addChangeListener(this);
this.renderer = renderer;
this.renderer.setPlot(this);
this.renderer.addChangeListener(this);
this.xAxis = xAxis;
this.xAxis.addChangeListener(this);
this.xAxis.configureAsXAxis(this);
this.zAxis = zAxis;
this.zAxis.addChangeListener(this);
this.zAxis.configureAsZAxis(this);
this.yAxis = yAxis;
this.yAxis.addChangeListener(this);
this.yAxis.configureAsYAxis(this);
this.gridlinesVisibleX = true;
this.gridlinePaintX = Color.WHITE;
this.gridlineStrokeX = DEFAULT_GRIDLINE_STROKE;
this.gridlinesVisibleY = true;
this.gridlinePaintY = Color.WHITE;
this.gridlineStrokeY = DEFAULT_GRIDLINE_STROKE;
this.gridlinesVisibleZ = true;
this.gridlinePaintZ = Color.WHITE;
this.gridlineStrokeZ = DEFAULT_GRIDLINE_STROKE;
this.legendLabelGenerator = new StandardXYZLabelGenerator();
this.toolTipGenerator = new StandardXYZItemLabelGenerator();
}
/**
* Sets the dimensions for the plot and notifies registered listeners that
* the plot dimensions have been changed.
*
* @param dim the new dimensions ({@code null} not permitted).
*/
public void setDimensions(Dimension3D dim) {
ArgChecks.nullNotPermitted(dim, "dim");
this.dimensions = dim;
fireChangeEvent(true);
}
/**
* Returns the dataset for the plot.
*
* @return The dataset (never {@code null}).
*/
public XYZDataset getDataset() {
return this.dataset;
}
/**
* Sets the dataset and sends a change event notification to all registered
* listeners.
*
* @param dataset the new dataset ({@code null} not permitted).
*/
public void setDataset(XYZDataset dataset) {
ArgChecks.nullNotPermitted(dataset, "dataset");
this.dataset.removeChangeListener(this);
this.dataset = dataset;
this.dataset.addChangeListener(this);
fireChangeEvent(true);
}
/**
* Returns the x-axis.
*
* @return The x-axis (never {@code null}).
*/
public ValueAxis3D getXAxis() {
return this.xAxis;
}
/**
* Sets the x-axis and sends a {@link Plot3DChangeEvent} to all registered
* listeners.
*
* @param xAxis the x-axis ({@code null} not permitted).
*/
public void setXAxis(ValueAxis3D xAxis) {
ArgChecks.nullNotPermitted(xAxis, "xAxis");
this.xAxis.removeChangeListener(this);
xAxis.configureAsXAxis(this);
xAxis.addChangeListener(this);
this.xAxis = xAxis;
fireChangeEvent(true);
}
/**
* Returns the y-axis.
*
* @return The y-axis (never {@code null}).
*/
public ValueAxis3D getYAxis() {
return this.yAxis;
}
/**
* Sets the y-axis and sends a {@link Plot3DChangeEvent} to all registered
* listeners.
*
* @param yAxis the y-axis ({@code null} not permitted).
*/
public void setYAxis(ValueAxis3D yAxis) {
ArgChecks.nullNotPermitted(yAxis, "yAxis");
this.yAxis.removeChangeListener(this);
yAxis.configureAsYAxis(this);
yAxis.addChangeListener(this);
this.yAxis = yAxis;
fireChangeEvent(true);
}
/**
* Returns the z-axis.
*
* @return The z-axis (never {@code null}).
*/
public ValueAxis3D getZAxis() {
return this.zAxis;
}
/**
* Sets the z-axis and sends a {@link Plot3DChangeEvent} to all registered
* listeners.
*
* @param zAxis the z-axis ({@code null} not permitted).
*/
public void setZAxis(ValueAxis3D zAxis) {
ArgChecks.nullNotPermitted(zAxis, "zAxis");
this.zAxis.removeChangeListener(this);
zAxis.configureAsZAxis(this);
zAxis.addChangeListener(this);
this.zAxis = zAxis;
fireChangeEvent(true);
}
/**
* Returns the renderer for the plot.
*
* @return The renderer (possibly {@code null}).
*/
public XYZRenderer getRenderer() {
return this.renderer;
}
/**
* Sets the renderer for the plot and sends a {@link Plot3DChangeEvent}
* to all registered listeners.
*
* @param renderer the renderer ({@code null} not permitted).
*/
public void setRenderer(XYZRenderer renderer) {
this.renderer.setPlot(null);
this.renderer.removeChangeListener(this);
this.renderer = renderer;
this.renderer.setPlot(this);
this.renderer.addChangeListener(this);
fireChangeEvent(true);
}
/**
* Returns the flag that controls whether or not gridlines are shown for
* the x-axis.
*
* @return A boolean.
*/
public boolean isGridlinesVisibleX() {
return this.gridlinesVisibleX;
}
/**
* Sets the flag that controls whether or not gridlines are shown for the
* x-axis and sends a {@link Plot3DChangeEvent} to all registered
* listeners.
*
* @param visible the new flag value.
*/
public void setGridlinesVisibleX(boolean visible) {
this.gridlinesVisibleX = visible;
fireChangeEvent(false);
}
/**
* Returns the paint used to draw the gridlines for the x-axis.
*
* @return The paint ({@code null} not permitted).
*/
public Paint getGridlinePaintX() {
return this.gridlinePaintX;
}
/**
* Sets the paint used to draw the gridlines for the x-axis, and sends
* a {@link Plot3DChangeEvent} to all registered listeners.
*
* @param paint the paint ({@code null} not permitted).
*/
public void setGridlinePaintX(Paint paint) {
ArgChecks.nullNotPermitted(paint, "paint");
this.gridlinePaintX = paint;
fireChangeEvent(false);
}
/**
* Returns the stroke used to draw the gridlines for the x-axis.
*
* @return The stroke ({@code null} not permitted).
*/
public Stroke getGridlineStrokeX() {
return this.gridlineStrokeX;
}
/**
* Sets the stroke used to draw the gridlines for the x-axis, and sends
* a {@link Plot3DChangeEvent} to all registered listeners.
*
* @param stroke the stroke ({@code null} not permitted).
*/
public void setGridlineStrokeX(Stroke stroke) {
ArgChecks.nullNotPermitted(stroke, "stroke");
this.gridlineStrokeX = stroke;
fireChangeEvent(false);
}
/**
* Returns the flag that controls whether or not gridlines are shown for
* the y-axis.
*
* @return A boolean.
*/
public boolean isGridlinesVisibleY() {
return this.gridlinesVisibleY;
}
/**
* Sets the flag that controls whether or not gridlines are shown for the
* y-axis and sends a {@link Plot3DChangeEvent} to all registered
* listeners.
*
* @param visible the new flag value.
*/
public void setGridlinesVisibleY(boolean visible) {
this.gridlinesVisibleY = visible;
fireChangeEvent(false);
}
/**
* Returns the paint used to draw the gridlines for the y-axis.
*
* @return The paint ({@code null} not permitted).
*/
public Paint getGridlinePaintY() {
return this.gridlinePaintY;
}
/**
* Sets the paint used to draw the gridlines for the y-axis, and sends
* a {@link Plot3DChangeEvent} to all registered listeners.
*
* @param paint the paint ({@code null} not permitted).
*/
public void setGridlinePaintY(Paint paint) {
ArgChecks.nullNotPermitted(paint, "paint");
this.gridlinePaintY = paint;
fireChangeEvent(false);
}
/**
* Returns the stroke used to draw the gridlines for the y-axis.
*
* @return The stroke ({@code null} not permitted).
*/
public Stroke getGridlineStrokeY() {
return this.gridlineStrokeY;
}
/**
* Sets the stroke used to draw the gridlines for the y-axis, and sends
* a {@link Plot3DChangeEvent} to all registered listeners.
*
* @param stroke the stroke ({@code null} not permitted).
*/
public void setGridlineStrokeY(Stroke stroke) {
ArgChecks.nullNotPermitted(stroke, "stroke");
this.gridlineStrokeY = stroke;
fireChangeEvent(false);
}
/**
* Returns the flag that controls whether or not gridlines are shown for
* the z-axis.
*
* @return A boolean.
*/
public boolean isGridlinesVisibleZ() {
return this.gridlinesVisibleZ;
}
/**
* Sets the flag that controls whether or not gridlines are shown for the
* z-axis and sends a {@link Plot3DChangeEvent} to all registered
* listeners.
*
* @param visible the new flag value.
*/
public void setGridlinesVisibleZ(boolean visible) {
this.gridlinesVisibleZ = visible;
fireChangeEvent(false);
}
/**
* Returns the paint used to draw the gridlines for the z-axis.
*
* @return The paint ({@code null} not permitted).
*/
public Paint getGridlinePaintZ() {
return this.gridlinePaintZ;
}
/**
* Sets the paint used to draw the gridlines for the z-axis, and sends
* a {@link Plot3DChangeEvent} to all registered listeners.
*
* @param paint the paint ({@code null} not permitted).
*/
public void setGridlinePaintZ(Paint paint) {
ArgChecks.nullNotPermitted(paint, "paint");
this.gridlinePaintZ = paint;
fireChangeEvent(false);
}
/**
* Returns the stroke used to draw the gridlines for the z-axis.
*
* @return The stroke ({@code null} not permitted).
*/
public Stroke getGridlineStrokeZ() {
return this.gridlineStrokeZ;
}
/**
* Sets the stroke used to draw the gridlines for the z-axis, and sends
* a {@link Plot3DChangeEvent} to all registered listeners.
*
* @param stroke the stroke ({@code null} not permitted).
*/
public void setGridlineStrokeZ(Stroke stroke) {
ArgChecks.nullNotPermitted(stroke, "stroke");
this.gridlineStrokeZ = stroke;
fireChangeEvent(false);
}
/**
* Returns the legend label generator. The default value is a default
* instance of {@link StandardXYZLabelGenerator}.
*
* @return The legend label generator (never {@code null}).
*
* @since 1.2
*/
public XYZLabelGenerator getLegendLabelGenerator() {
return this.legendLabelGenerator;
}
/**
* Sets the legend label generator and sends a {@link Plot3DChangeEvent}
* to all registered listeners.
*
* @param generator the generator ({@code null} not permitted).
*
* @since 1.2
*/
public void setLegendLabelGenerator(XYZLabelGenerator generator) {
ArgChecks.nullNotPermitted(generator, "generator");
this.legendLabelGenerator = generator;
fireChangeEvent(false);
}
/**
* Returns a list containing legend item info, typically one item for
* each series in the chart. This is intended for use in the construction
* of a chart legend.
*
* @return A list containing legend item info.
*/
@Override
@SuppressWarnings("unchecked") // we don't know the generic types of the dataset
public List<LegendItemInfo> getLegendInfo() {
List<LegendItemInfo> result = new ArrayList<LegendItemInfo>();
List<Comparable<?>> keys = this.dataset.getSeriesKeys();
for (Comparable key : keys) {
String label = this.legendLabelGenerator.generateSeriesLabel(
this.dataset, (Comparable) key);
int series = this.dataset.getSeriesIndex(key);
Color color = this.renderer.getColorSource().getLegendColor(series);
LegendItemInfo info = new StandardLegendItemInfo(key, label, color);
result.add(info);
}
return result;
}
/**
* Adds 3D objects representing the current data for the plot to the
* specified world. After the world has been populated (or constructed) in
* this way, it is ready for rendering.
*
* @param world the world ({@code null} not permitted).
* @param xOffset the x-offset.
* @param yOffset the y-offset.
* @param zOffset the z-offset.
*/
@Override
public void compose(World world, double xOffset, double yOffset,
double zOffset) {
if (this.renderer.getComposeType() == ComposeType.ALL) {
this.renderer.composeAll(this, world, this.dimensions, xOffset,
yOffset, zOffset);
} else if (this.renderer.getComposeType() == ComposeType.PER_ITEM) {
// for each data point in the dataset figure out if the composed
// shape intersects with the visible
// subset of the world, and if so add the object
int seriesCount = this.dataset.getSeriesCount();
for (int series = 0; series < seriesCount; series++) {
int itemCount = this.dataset.getItemCount(series);
for (int item = 0; item < itemCount; item++) {
this.renderer.composeItem(this.dataset, series, item, world,
this.dimensions, xOffset, yOffset, zOffset);
}
}
} else {
// if we get here, someone changed the ComposeType enum
throw new IllegalStateException("ComposeType not expected: "
+ this.renderer.getComposeType());
}
}
@Override
public String generateToolTipText(ItemKey itemKey) {
if (!(itemKey instanceof XYZItemKey)) {
throw new IllegalArgumentException(
"The itemKey must be a XYZItemKey instance.");
}
if (this.toolTipGenerator == null) {
return null;
}
XYZItemKey k = (XYZItemKey) itemKey;
return this.toolTipGenerator.generateItemLabel(dataset,
k.getSeriesKey(), k.getItemIndex());
}
/**
* Receives a visitor. This is a general purpose mechanism, but the main
* use is to apply chart style changes across all the elements of a
* chart.
*
* @param visitor the visitor ({@code null} not permitted).
*
* @since 1.2
*/
@Override
public void receive(ChartElementVisitor visitor) {
this.xAxis.receive(visitor);
this.yAxis.receive(visitor);
this.zAxis.receive(visitor);
this.renderer.receive(visitor);
visitor.visit(this);
}
/**
* Tests this plot instance for equality with an arbitrary object.
*
* @param obj the object ({@code null} permitted).
*
* @return A boolean.
*/
@Override
public boolean equals(Object obj) {
if (obj == this) {
return true;
}
if (!(obj instanceof XYZPlot)) {
return false;
}
XYZPlot that = (XYZPlot) obj;
if (!this.dimensions.equals(that.dimensions)) {
return false;
}
if (this.gridlinesVisibleX != that.gridlinesVisibleX) {
return false;
}
if (this.gridlinesVisibleY != that.gridlinesVisibleY) {
return false;
}
if (this.gridlinesVisibleZ != that.gridlinesVisibleZ) {
return false;
}
if (!ObjectUtils.equalsPaint(this.gridlinePaintX,
that.gridlinePaintX)) {
return false;
}
if (!ObjectUtils.equalsPaint(this.gridlinePaintY,
that.gridlinePaintY)) {
return false;
}
if (!ObjectUtils.equalsPaint(this.gridlinePaintZ,
that.gridlinePaintZ)) {
return false;
}
if (!this.gridlineStrokeX.equals(that.gridlineStrokeX)) {
return false;
}
if (!this.gridlineStrokeY.equals(that.gridlineStrokeY)) {
return false;
}
if (!this.gridlineStrokeZ.equals(that.gridlineStrokeZ)) {
return false;
}
if (!this.legendLabelGenerator.equals(that.legendLabelGenerator)) {
return false;
}
return super.equals(obj);
}
/**
* Receives notification that one of the plot's axes has changed, and
* responds by passing on a {@link Plot3DChangeEvent} to the plot's
* registered listeners (with the default set-up, this notifies the
* chart).
*
* @param event the event.
*/
@Override
public void axisChanged(Axis3DChangeEvent event) {
this.yAxis.configureAsYAxis(this);
fireChangeEvent(event.requiresWorldUpdate());
}
/**
* Receives notification that the plot's renderer has changed, and
* responds by passing on a {@link Plot3DChangeEvent} to the plot's
* registered listeners (with the default set-up, this notifies the
* chart).
*
* @param event the event.
*/
@Override
public void rendererChanged(Renderer3DChangeEvent event) {
fireChangeEvent(event.requiresWorldUpdate());
}
/**
* Receives notification that the plot's dataset has changed, and
* responds by passing on a {@link Plot3DChangeEvent} to the plot's
* registered listeners (with the default set-up, this notifies the
* chart).
*
* @param event the event.
*/
@Override
public void datasetChanged(Dataset3DChangeEvent event) {
this.xAxis.configureAsXAxis(this);
this.yAxis.configureAsYAxis(this);
this.zAxis.configureAsZAxis(this);
super.datasetChanged(event);
}
/**
* Provides serialization support.
*
* @param stream the output stream.
*
* @throws IOException if there is an I/O error.
*/
private void writeObject(ObjectOutputStream stream) throws IOException {
stream.defaultWriteObject();
SerialUtils.writePaint(this.gridlinePaintX, stream);
SerialUtils.writePaint(this.gridlinePaintY, stream);
SerialUtils.writePaint(this.gridlinePaintZ, stream);
SerialUtils.writeStroke(this.gridlineStrokeX, stream);
SerialUtils.writeStroke(this.gridlineStrokeY, stream);
SerialUtils.writeStroke(this.gridlineStrokeZ, stream);
}
/**
* Provides serialization support.
*
* @param stream the input stream.
*
* @throws IOException if there is an I/O error.
* @throws ClassNotFoundException if there is a classpath problem.
*/
private void readObject(ObjectInputStream stream)
throws IOException, ClassNotFoundException {
stream.defaultReadObject();
this.gridlinePaintX = SerialUtils.readPaint(stream);
this.gridlinePaintY = SerialUtils.readPaint(stream);
this.gridlinePaintZ = SerialUtils.readPaint(stream);
this.gridlineStrokeX = SerialUtils.readStroke(stream);
this.gridlineStrokeY = SerialUtils.readStroke(stream);
this.gridlineStrokeZ = SerialUtils.readStroke(stream);
}
}