001/* ==================================================== 002 * Orson Charts FX : JavaFX extensions for Orson Charts 003 * ==================================================== 004 * 005 * Copyright 2013-present, by David Gilbert. 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 * license is available to sponsors (higher tiers only) of the JFree projects. 027 * For details, please see visit: 028 * 029 * https://github.com/sponsors/jfree 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(this::updateTooltip); 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(this::handleMouseDragged); 138 setOnScroll(this::handleScroll); 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 rotation 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 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 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 rotation by mouse dragging is enabled. 309 * 310 * @return A boolean. 311 */ 312 public boolean isRotateViewEnabled() { 313 return this.rotateViewEnabled; 314 } 315 316 /** 317 * Sets the flag that controls whether rotation by mouse dragging is enabled. 318 * 319 * @param enabled the new flag value. 320 */ 321 public void setRotateViewEnabled(boolean enabled) { 322 this.rotateViewEnabled = enabled; 323 } 324 325 /** 326 * Adjusts the viewing distance so that the chart fits the specified 327 * size. A margin is left (see {@link #getMargin()}) around the edges to 328 * leave room for labels etc. 329 * 330 * @param width the width. 331 * @param height the height. 332 */ 333 public void zoomToFit(double width, double height) { 334 int w = (int) (width * (1.0 - this.margin)); 335 int h = (int) (height * (1.0 - this.margin)); 336 Dimension2D target = new Dimension(w, h); 337 Dimension3D d3d = this.chart.getDimensions(); 338 float distance = this.chart.getViewPoint().optimalDistance(target, 339 d3d, this.chart.getProjDistance()); 340 this.chart.getViewPoint().setRho(distance); 341 draw(); 342 } 343 344 /** 345 * Draws the content of the canvas and updates the 346 * {@code renderingInfo} attribute with the latest rendering 347 * information. 348 */ 349 public void draw() { 350 GraphicsContext ctx = getGraphicsContext2D(); 351 ctx.save(); 352 double width = getWidth(); 353 double height = getHeight(); 354 if (width > 0 && height > 0) { 355 ctx.clearRect(0, 0, width, height); 356 this.renderingInfo = this.chart.draw(this.g2, 357 new Rectangle((int) width, (int) height)); 358 } 359 ctx.restore(); 360 } 361 362 /** 363 * Return {@code true} to indicate the canvas is resizable. 364 * 365 * @return {@code true}. 366 */ 367 @Override 368 public boolean isResizable() { 369 return true; 370 } 371 372 /** 373 * Updates the tooltip. This method will return without doing anything if 374 * the {@code tooltipEnabled} flag is set to false. 375 * 376 * @param me the mouse event. 377 */ 378 protected void updateTooltip(MouseEvent me) { 379 if (!this.tooltipEnabled || this.renderingInfo == null) { 380 return; 381 } 382 Object3D object = this.renderingInfo.fetchObjectAt(me.getX(), 383 me.getY()); 384 if (object != null) { 385 ItemKey key = (ItemKey) object.getProperty(Object3D.ITEM_KEY); 386 if (key != null) { 387 String toolTipText = chart.getPlot().generateToolTipText(key); 388 if (this.tooltip == null) { 389 this.tooltip = new Tooltip(toolTipText); 390 Tooltip.install(this, this.tooltip); 391 } else { 392 this.tooltip.setText(toolTipText); 393 this.tooltip.setAnchorX(me.getScreenX()); 394 this.tooltip.setAnchorY(me.getScreenY()); 395 } 396 } else { 397 if (this.tooltip != null) { 398 Tooltip.uninstall(this, this.tooltip); 399 } 400 this.tooltip = null; 401 } 402 } 403 } 404 405 /** 406 * Handles a mouse dragged event by rotating the chart (unless the 407 * {@code rotateViewEnabled} flag is set to false, in which case this 408 * method does nothing). 409 * 410 * @param event the mouse event. 411 */ 412 private void handleMouseDragged(MouseEvent event) { 413 if (!this.rotateViewEnabled) { 414 return; 415 } 416 Point currPt = new Point((int) event.getScreenX(), 417 (int) event.getScreenY()); 418 int dx = currPt.x - this.lastMovePoint.x; 419 int dy = currPt.y - this.lastMovePoint.y; 420 this.lastMovePoint = currPt; 421 this.chart.getViewPoint().panLeftRight(-dx * this.panIncrement); 422 this.chart.getViewPoint().moveUpDown(-dy * this.rotateIncrement); 423 this.draw(); 424 } 425 426 private void handleScroll(ScrollEvent event) { 427 double units = -event.getDeltaY(); 428 double maxViewingDistance = this.maxViewingDistanceMultiplier 429 * this.minViewingDistance; 430 ViewPoint3D vp = this.chart.getViewPoint(); 431 double valRho = Math.max(this.minViewingDistance, 432 Math.min(maxViewingDistance, vp.getRho() + units)); 433 vp.setRho(valRho); 434 draw(); 435 } 436 437 /** 438 * Redraws the chart whenever a chart change event is received. 439 * 440 * @param event the event ({@code null} not permitted). 441 */ 442 @Override 443 public void chartChanged(Chart3DChangeEvent event) { 444 draw(); 445 } 446}