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