/* ===========================================================
* 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.fx;
import java.awt.Dimension;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.geom.Dimension2D;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.control.Tooltip;
import javafx.scene.input.MouseEvent;
import javafx.scene.input.ScrollEvent;
import com.orsoncharts.Chart3D;
import com.orsoncharts.Chart3DChangeEvent;
import com.orsoncharts.Chart3DChangeListener;
import com.orsoncharts.data.ItemKey;
import com.orsoncharts.graphics3d.Dimension3D;
import com.orsoncharts.graphics3d.Object3D;
import com.orsoncharts.graphics3d.Offset2D;
import com.orsoncharts.graphics3d.RenderingInfo;
import com.orsoncharts.graphics3d.ViewPoint3D;
import com.orsoncharts.util.ArgChecks;
import org.jfree.fx.FXGraphics2D;
/**
* A canvas node for displaying a {@link Chart3D} in JavaFX. This node
* handles mouse events and tooltips but does not provide a context menu or
* toolbar (these features are provided by the {@link Chart3DViewer} class.)
*
* @since 1.4
*/
public class Chart3DCanvas extends Canvas implements Chart3DChangeListener {
/** The chart being displayed in the canvas. */
private Chart3D chart;
/**
* The graphics drawing context (will be an instance of FXGraphics2D).
*/
private Graphics2D g2;
/** The rendering info from the last drawing of the chart. */
private RenderingInfo renderingInfo;
/**
* The minimum viewing distance (zooming in will not go closer than this).
*/
private double minViewingDistance;
/**
* The multiplier for the maximum viewing distance (a multiple of the
* minimum viewing distance).
*/
private double maxViewingDistanceMultiplier;
/**
* The margin around the chart (used when zooming to fit).
*/
private double margin = 0.25;
/** The angle increment for panning left and right (in radians). */
private double panIncrement = Math.PI / 120.0;
/** The angle increment for rotating up and down (in radians). */
private double rotateIncrement = Math.PI / 120.0;
/**
* The (screen) point of the last mouse click (will be {@code null}
* initially). Used to calculate the mouse drag distance and direction.
*/
private Point lastClickPoint;
/**
* The (screen) point of the last mouse move point that was handled.
*/
private Point lastMovePoint;
/**
* Temporary state to track the 2D offset during an ALT-mouse-drag
* operation.
*/
private Offset2D offsetAtMousePressed;
/** The tooltip object for the canvas. */
private Tooltip tooltip;
/** Are tooltips enabled? */
private boolean tooltipEnabled = true;
/** Is rotation by mouse-dragging enabled? */
private boolean rotateViewEnabled = true;
/**
* Creates a new canvas to display the supplied chart in JavaFX.
*
* @param chart the chart ({@code null} not permitted).
*/
public Chart3DCanvas(Chart3D chart) {
this.chart = chart;
this.minViewingDistance = chart.getDimensions().getDiagonalLength();
this.maxViewingDistanceMultiplier = 8.0;
widthProperty().addListener(e -> draw());
heightProperty().addListener(e -> draw());
this.g2 = new FXGraphics2D(getGraphicsContext2D());
setOnMouseMoved((MouseEvent me) -> { updateTooltip(me); });
setOnMousePressed((MouseEvent me) -> {
Chart3DCanvas canvas = Chart3DCanvas.this;
canvas.lastClickPoint = new Point((int) me.getScreenX(),
(int) me.getScreenY());
canvas.lastMovePoint = canvas.lastClickPoint;
});
setOnMouseDragged((MouseEvent me) -> { handleMouseDragged(me); });
setOnScroll((ScrollEvent event) -> { handleScroll(event); });
this.chart.addChangeListener(this);
}
/**
* Returns the chart that is being displayed by this node.
*
* @return The chart (never {@code null}).
*/
public Chart3D getChart() {
return this.chart;
}
/**
* Sets the chart to be displayed by this node.
*
* @param chart the chart ({@code null} not permitted).
*/
public void setChart(Chart3D chart) {
ArgChecks.nullNotPermitted(chart, "chart");
if (this.chart != null) {
this.chart.removeChangeListener(this);
}
this.chart = chart;
this.chart.addChangeListener(this);
draw();
}
/**
* Returns the margin that is used when zooming to fit. The margin can
* be used to control the amount of space around the chart (where labels
* are often drawn). The default value is 0.25 (25 percent).
*
* @return The margin.
*/
public double getMargin() {
return this.margin;
}
/**
* Sets the margin (note that this will not have an immediate effect, it
* will only be applied on the next call to
* {@link #zoomToFit(double, double)}).
*
* @param margin the margin.
*/
public void setMargin(double margin) {
this.margin = margin;
}
/**
* Returns the rendering info from the most recent drawing of the chart.
*
* @return The rendering info (possibly {@code null}).
*/
public RenderingInfo getRenderingInfo() {
return this.renderingInfo;
}
/**
* Returns the minimum distance between the viewing point and the origin.
* This is initialised in the constructor based on the chart dimensions.
*
* @return The minimum viewing distance.
*/
public double getMinViewingDistance() {
return this.minViewingDistance;
}
/**
* Sets the minimum between the viewing point and the origin. If the
* current distance is lower than the new minimum, it will be set to this
* minimum value.
*
* @param minViewingDistance the minimum viewing distance.
*/
public void setMinViewingDistance(double minViewingDistance) {
this.minViewingDistance = minViewingDistance;
if (this.chart.getViewPoint().getRho() < this.minViewingDistance) {
this.chart.getViewPoint().setRho(this.minViewingDistance);
}
}
/**
* Returns the multiplier used to calculate the maximum permitted distance
* between the viewing point and the origin. The multiplier is applied to
* the minimum viewing distance. The default value is 8.0.
*
* @return The multiplier.
*/
public double getMaxViewingDistanceMultiplier() {
return this.maxViewingDistanceMultiplier;
}
/**
* Sets the multiplier used to calculate the maximum viewing distance.
*
* @param multiplier the multiplier (must be > 1.0).
*/
public void setMaxViewingDistanceMultiplier(double multiplier) {
if (multiplier < 1.0) {
throw new IllegalArgumentException(
"The 'multiplier' should be greater than 1.0.");
}
this.maxViewingDistanceMultiplier = multiplier;
double maxDistance = this.minViewingDistance * multiplier;
if (this.chart.getViewPoint().getRho() > maxDistance) {
this.chart.getViewPoint().setRho(maxDistance);
}
}
/**
* Returns the increment for panning left and right. This is an angle in
* radians, and the default value is {@code Math.PI / 120.0}.
*
* @return The panning increment.
*/
public double getPanIncrement() {
return this.panIncrement;
}
/**
* Sets the increment for panning left and right (an angle measured in
* radians).
*
* @param increment the angle in radians.
*/
public void setPanIncrement(double increment) {
this.panIncrement = increment;
}
/**
* Returns the increment for rotating up and down. This is an angle in
* radians, and the default value is {@code Math.PI / 120.0}.
*
* @return The rotate increment.
*/
public double getRotateIncrement() {
return this.rotateIncrement;
}
/**
* Sets the increment for rotating up and down (an angle measured in
* radians).
*
* @param increment the angle in radians.
*/
public void setRotateIncrement(double increment) {
this.rotateIncrement = increment;
}
/**
* Returns the flag that controls whether or not tooltips are enabled.
*
* @return The flag.
*/
public boolean isTooltipEnabled() {
return this.tooltipEnabled;
}
/**
* Sets the flag that controls whether or not tooltips are enabled.
*
* @param tooltipEnabled the new flag value.
*/
public void setTooltipEnabled(boolean tooltipEnabled) {
this.tooltipEnabled = tooltipEnabled;
}
/**
* Returns a flag that controls whether or not rotation by mouse dragging
* is enabled.
*
* @return A boolean.
*/
public boolean isRotateViewEnabled() {
return this.rotateViewEnabled;
}
/**
* Sets the flag that controls whether or not rotation by mouse dragging
* is enabled.
*
* @param enabled the new flag value.
*/
public void setRotateViewEnabled(boolean enabled) {
this.rotateViewEnabled = enabled;
}
/**
* Adjusts the viewing distance so that the chart fits the specified
* size. A margin is left (see {@link #getMargin()}) around the edges to
* leave room for labels etc.
*
* @param width the width.
* @param height the height.
*/
public void zoomToFit(double width, double height) {
int w = (int) (width * (1.0 - this.margin));
int h = (int) (height * (1.0 - this.margin));
Dimension2D target = new Dimension(w, h);
Dimension3D d3d = this.chart.getDimensions();
float distance = this.chart.getViewPoint().optimalDistance(target,
d3d, this.chart.getProjDistance());
this.chart.getViewPoint().setRho(distance);
draw();
}
/**
* Draws the content of the canvas and updates the
* {@code renderingInfo} attribute with the latest rendering
* information.
*/
public void draw() {
GraphicsContext ctx = getGraphicsContext2D();
ctx.save();
double width = getWidth();
double height = getHeight();
if (width > 0 && height > 0) {
ctx.clearRect(0, 0, width, height);
this.renderingInfo = this.chart.draw(this.g2,
new Rectangle((int) width, (int) height));
}
ctx.restore();
}
/**
* Return {@code true} to indicate the canvas is resizable.
*
* @return {@code true}.
*/
@Override
public boolean isResizable() {
return true;
}
/**
* Updates the tooltip. This method will return without doing anything if
* the {@code tooltipEnabled} flag is set to false.
*
* @param me the mouse event.
*/
protected void updateTooltip(MouseEvent me) {
if (!this.tooltipEnabled || this.renderingInfo == null) {
return;
}
Object3D object = this.renderingInfo.fetchObjectAt(me.getX(),
me.getY());
if (object != null) {
ItemKey key = (ItemKey) object.getProperty(Object3D.ITEM_KEY);
if (key != null) {
String toolTipText = chart.getPlot().generateToolTipText(key);
if (this.tooltip == null) {
this.tooltip = new Tooltip(toolTipText);
Tooltip.install(this, this.tooltip);
} else {
this.tooltip.setText(toolTipText);
this.tooltip.setAnchorX(me.getScreenX());
this.tooltip.setAnchorY(me.getScreenY());
}
} else {
if (this.tooltip != null) {
Tooltip.uninstall(this, this.tooltip);
}
this.tooltip = null;
}
}
}
/**
* Handles a mouse dragged event by rotating the chart (unless the
* {@code rotateViewEnabled} flag is set to false, in which case this
* method does nothing).
*
* @param event the mouse event.
*/
private void handleMouseDragged(MouseEvent event) {
if (!this.rotateViewEnabled) {
return;
}
Point currPt = new Point((int) event.getScreenX(),
(int) event.getScreenY());
int dx = currPt.x - this.lastMovePoint.x;
int dy = currPt.y - this.lastMovePoint.y;
this.lastMovePoint = currPt;
this.chart.getViewPoint().panLeftRight(-dx * this.panIncrement);
this.chart.getViewPoint().moveUpDown(-dy * this.rotateIncrement);
this.draw();
}
private void handleScroll(ScrollEvent event) {
double units = -event.getDeltaY();
double maxViewingDistance = this.maxViewingDistanceMultiplier
* this.minViewingDistance;
ViewPoint3D vp = this.chart.getViewPoint();
double valRho = Math.max(this.minViewingDistance,
Math.min(maxViewingDistance, vp.getRho() + units));
vp.setRho(valRho);
draw();
}
@Override
public void chartChanged(Chart3DChangeEvent event) {
draw();
}
}