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.fx; 034 035import java.awt.Dimension; 036import java.awt.Graphics2D; 037import java.awt.Point; 038import java.awt.Rectangle; 039import java.awt.geom.Dimension2D; 040import javafx.scene.canvas.Canvas; 041import javafx.scene.canvas.GraphicsContext; 042import javafx.scene.control.Tooltip; 043import javafx.scene.input.MouseEvent; 044import javafx.scene.input.ScrollEvent; 045import com.orsoncharts.Chart3D; 046import com.orsoncharts.Chart3DChangeEvent; 047import com.orsoncharts.Chart3DChangeListener; 048import com.orsoncharts.data.ItemKey; 049import com.orsoncharts.graphics3d.Dimension3D; 050import com.orsoncharts.graphics3d.Object3D; 051import com.orsoncharts.graphics3d.Offset2D; 052import com.orsoncharts.graphics3d.RenderingInfo; 053import com.orsoncharts.graphics3d.ViewPoint3D; 054import com.orsoncharts.util.ArgChecks; 055import org.jfree.fx.FXGraphics2D; 056 057/** 058 * A canvas node for displaying a {@link Chart3D} in JavaFX. This node 059 * handles mouse events and tooltips but does not provide a context menu or 060 * toolbar (these features are provided by the {@link Chart3DViewer} class.) 061 * 062 * @since 1.4 063 */ 064public class Chart3DCanvas extends Canvas implements Chart3DChangeListener { 065 066 /** The chart being displayed in the canvas. */ 067 private Chart3D chart; 068 069 /** 070 * The graphics drawing context (will be an instance of FXGraphics2D). 071 */ 072 private Graphics2D g2; 073 074 /** The rendering info from the last drawing of the chart. */ 075 private RenderingInfo renderingInfo; 076 077 /** 078 * The minimum viewing distance (zooming in will not go closer than this). 079 */ 080 private double minViewingDistance; 081 082 /** 083 * The multiplier for the maximum viewing distance (a multiple of the 084 * minimum viewing distance). 085 */ 086 private double maxViewingDistanceMultiplier; 087 088 /** 089 * The margin around the chart (used when zooming to fit). 090 */ 091 private double margin = 0.25; 092 093 /** The angle increment for panning left and right (in radians). */ 094 private double panIncrement = Math.PI / 120.0; 095 096 /** The angle increment for rotating up and down (in radians). */ 097 private double rotateIncrement = Math.PI / 120.0; 098 099 /** 100 * The (screen) point of the last mouse click (will be {@code null} 101 * initially). Used to calculate the mouse drag distance and direction. 102 */ 103 private Point lastClickPoint; 104 105 /** 106 * The (screen) point of the last mouse move point that was handled. 107 */ 108 private Point lastMovePoint; 109 110 /** 111 * Temporary state to track the 2D offset during an ALT-mouse-drag 112 * operation. 113 */ 114 private Offset2D offsetAtMousePressed; 115 116 /** The tooltip object for the canvas. */ 117 private Tooltip tooltip; 118 119 /** Are tooltips enabled? */ 120 private boolean tooltipEnabled = true; 121 122 /** Is rotation by mouse-dragging enabled? */ 123 private boolean rotateViewEnabled = true; 124 125 /** 126 * Creates a new canvas to display the supplied chart in JavaFX. 127 * 128 * @param chart the chart ({@code null} not permitted). 129 */ 130 public Chart3DCanvas(Chart3D chart) { 131 this.chart = chart; 132 this.minViewingDistance = chart.getDimensions().getDiagonalLength(); 133 this.maxViewingDistanceMultiplier = 8.0; 134 widthProperty().addListener(e -> draw()); 135 heightProperty().addListener(e -> draw()); 136 this.g2 = new FXGraphics2D(getGraphicsContext2D()); 137 138 setOnMouseMoved((MouseEvent me) -> { updateTooltip(me); }); 139 setOnMousePressed((MouseEvent me) -> { 140 Chart3DCanvas canvas = Chart3DCanvas.this; 141 canvas.lastClickPoint = new Point((int) me.getScreenX(), 142 (int) me.getScreenY()); 143 canvas.lastMovePoint = canvas.lastClickPoint; 144 }); 145 146 setOnMouseDragged((MouseEvent me) -> { handleMouseDragged(me); }); 147 setOnScroll((ScrollEvent event) -> { handleScroll(event); }); 148 this.chart.addChangeListener(this); 149 } 150 151 /** 152 * Returns the chart that is being displayed by this node. 153 * 154 * @return The chart (never {@code null}). 155 */ 156 public Chart3D getChart() { 157 return this.chart; 158 } 159 160 /** 161 * Sets the chart to be displayed by this node. 162 * 163 * @param chart the chart ({@code null} not permitted). 164 */ 165 public void setChart(Chart3D chart) { 166 ArgChecks.nullNotPermitted(chart, "chart"); 167 if (this.chart != null) { 168 this.chart.removeChangeListener(this); 169 } 170 this.chart = chart; 171 this.chart.addChangeListener(this); 172 draw(); 173 } 174 175 /** 176 * Returns the margin that is used when zooming to fit. The margin can 177 * be used to control the amount of space around the chart (where labels 178 * are often drawn). The default value is 0.25 (25 percent). 179 * 180 * @return The margin. 181 */ 182 public double getMargin() { 183 return this.margin; 184 } 185 186 /** 187 * Sets the margin (note that this will not have an immediate effect, it 188 * will only be applied on the next call to 189 * {@link #zoomToFit(double, double)}). 190 * 191 * @param margin the margin. 192 */ 193 public void setMargin(double margin) { 194 this.margin = margin; 195 } 196 197 /** 198 * Returns the rendering info from the most recent drawing of the chart. 199 * 200 * @return The rendering info (possibly {@code null}). 201 */ 202 public RenderingInfo getRenderingInfo() { 203 return this.renderingInfo; 204 } 205 206 /** 207 * Returns the minimum distance between the viewing point and the origin. 208 * This is initialised in the constructor based on the chart dimensions. 209 * 210 * @return The minimum viewing distance. 211 */ 212 public double getMinViewingDistance() { 213 return this.minViewingDistance; 214 } 215 216 /** 217 * Sets the minimum between the viewing point and the origin. If the 218 * current distance is lower than the new minimum, it will be set to this 219 * minimum value. 220 * 221 * @param minViewingDistance the minimum viewing distance. 222 */ 223 public void setMinViewingDistance(double minViewingDistance) { 224 this.minViewingDistance = minViewingDistance; 225 if (this.chart.getViewPoint().getRho() < this.minViewingDistance) { 226 this.chart.getViewPoint().setRho(this.minViewingDistance); 227 } 228 } 229 230 /** 231 * Returns the multiplier used to calculate the maximum permitted distance 232 * between the viewing point and the origin. The multiplier is applied to 233 * the minimum viewing distance. The default value is 8.0. 234 * 235 * @return The multiplier. 236 */ 237 public double getMaxViewingDistanceMultiplier() { 238 return this.maxViewingDistanceMultiplier; 239 } 240 241 /** 242 * Sets the multiplier used to calculate the maximum viewing distance. 243 * 244 * @param multiplier the multiplier (must be > 1.0). 245 */ 246 public void setMaxViewingDistanceMultiplier(double multiplier) { 247 if (multiplier < 1.0) { 248 throw new IllegalArgumentException( 249 "The 'multiplier' should be greater than 1.0."); 250 } 251 this.maxViewingDistanceMultiplier = multiplier; 252 double maxDistance = this.minViewingDistance * multiplier; 253 if (this.chart.getViewPoint().getRho() > maxDistance) { 254 this.chart.getViewPoint().setRho(maxDistance); 255 } 256 } 257 258 /** 259 * Returns the increment for panning left and right. This is an angle in 260 * radians, and the default value is {@code Math.PI / 120.0}. 261 * 262 * @return The panning increment. 263 */ 264 public double getPanIncrement() { 265 return this.panIncrement; 266 } 267 268 /** 269 * Sets the increment for panning left and right (an angle measured in 270 * radians). 271 * 272 * @param increment the angle in radians. 273 */ 274 public void setPanIncrement(double increment) { 275 this.panIncrement = increment; 276 } 277 278 /** 279 * Returns the increment for rotating up and down. This is an angle in 280 * radians, and the default value is {@code Math.PI / 120.0}. 281 * 282 * @return The rotate increment. 283 */ 284 public double getRotateIncrement() { 285 return this.rotateIncrement; 286 } 287 288 /** 289 * Sets the increment for rotating up and down (an angle measured in 290 * radians). 291 * 292 * @param increment the angle in radians. 293 */ 294 public void setRotateIncrement(double increment) { 295 this.rotateIncrement = increment; 296 } 297 298 /** 299 * Returns the flag that controls whether or not tooltips are enabled. 300 * 301 * @return The flag. 302 */ 303 public boolean isTooltipEnabled() { 304 return this.tooltipEnabled; 305 } 306 307 /** 308 * Sets the flag that controls whether or not tooltips are enabled. 309 * 310 * @param tooltipEnabled the new flag value. 311 */ 312 public void setTooltipEnabled(boolean tooltipEnabled) { 313 this.tooltipEnabled = tooltipEnabled; 314 } 315 316 /** 317 * Returns a flag that controls whether or not rotation by mouse dragging 318 * is enabled. 319 * 320 * @return A boolean. 321 */ 322 public boolean isRotateViewEnabled() { 323 return this.rotateViewEnabled; 324 } 325 326 /** 327 * Sets the flag that controls whether or not rotation by mouse dragging 328 * is enabled. 329 * 330 * @param enabled the new flag value. 331 */ 332 public void setRotateViewEnabled(boolean enabled) { 333 this.rotateViewEnabled = enabled; 334 } 335 336 /** 337 * Adjusts the viewing distance so that the chart fits the specified 338 * size. A margin is left (see {@link #getMargin()}) around the edges to 339 * leave room for labels etc. 340 * 341 * @param width the width. 342 * @param height the height. 343 */ 344 public void zoomToFit(double width, double height) { 345 int w = (int) (width * (1.0 - this.margin)); 346 int h = (int) (height * (1.0 - this.margin)); 347 Dimension2D target = new Dimension(w, h); 348 Dimension3D d3d = this.chart.getDimensions(); 349 float distance = this.chart.getViewPoint().optimalDistance(target, 350 d3d, this.chart.getProjDistance()); 351 this.chart.getViewPoint().setRho(distance); 352 draw(); 353 } 354 355 /** 356 * Draws the content of the canvas and updates the 357 * {@code renderingInfo} attribute with the latest rendering 358 * information. 359 */ 360 public void draw() { 361 GraphicsContext ctx = getGraphicsContext2D(); 362 ctx.save(); 363 double width = getWidth(); 364 double height = getHeight(); 365 if (width > 0 && height > 0) { 366 ctx.clearRect(0, 0, width, height); 367 this.renderingInfo = this.chart.draw(this.g2, 368 new Rectangle((int) width, (int) height)); 369 } 370 ctx.restore(); 371 } 372 373 /** 374 * Return {@code true} to indicate the canvas is resizable. 375 * 376 * @return {@code true}. 377 */ 378 @Override 379 public boolean isResizable() { 380 return true; 381 } 382 383 /** 384 * Updates the tooltip. This method will return without doing anything if 385 * the {@code tooltipEnabled} flag is set to false. 386 * 387 * @param me the mouse event. 388 */ 389 protected void updateTooltip(MouseEvent me) { 390 if (!this.tooltipEnabled || this.renderingInfo == null) { 391 return; 392 } 393 Object3D object = this.renderingInfo.fetchObjectAt(me.getX(), 394 me.getY()); 395 if (object != null) { 396 ItemKey key = (ItemKey) object.getProperty(Object3D.ITEM_KEY); 397 if (key != null) { 398 String toolTipText = chart.getPlot().generateToolTipText(key); 399 if (this.tooltip == null) { 400 this.tooltip = new Tooltip(toolTipText); 401 Tooltip.install(this, this.tooltip); 402 } else { 403 this.tooltip.setText(toolTipText); 404 this.tooltip.setAnchorX(me.getScreenX()); 405 this.tooltip.setAnchorY(me.getScreenY()); 406 } 407 } else { 408 if (this.tooltip != null) { 409 Tooltip.uninstall(this, this.tooltip); 410 } 411 this.tooltip = null; 412 } 413 } 414 } 415 416 /** 417 * Handles a mouse dragged event by rotating the chart (unless the 418 * {@code rotateViewEnabled} flag is set to false, in which case this 419 * method does nothing). 420 * 421 * @param event the mouse event. 422 */ 423 private void handleMouseDragged(MouseEvent event) { 424 if (!this.rotateViewEnabled) { 425 return; 426 } 427 Point currPt = new Point((int) event.getScreenX(), 428 (int) event.getScreenY()); 429 int dx = currPt.x - this.lastMovePoint.x; 430 int dy = currPt.y - this.lastMovePoint.y; 431 this.lastMovePoint = currPt; 432 this.chart.getViewPoint().panLeftRight(-dx * this.panIncrement); 433 this.chart.getViewPoint().moveUpDown(-dy * this.rotateIncrement); 434 this.draw(); 435 } 436 437 private void handleScroll(ScrollEvent event) { 438 double units = -event.getDeltaY(); 439 double maxViewingDistance = this.maxViewingDistanceMultiplier 440 * this.minViewingDistance; 441 ViewPoint3D vp = this.chart.getViewPoint(); 442 double valRho = Math.max(this.minViewingDistance, 443 Math.min(maxViewingDistance, vp.getRho() + units)); 444 vp.setRho(valRho); 445 draw(); 446 } 447 448 @Override 449 public void chartChanged(Chart3DChangeEvent event) { 450 draw(); 451 } 452} 453