001/* =========================================================== 002 * Orson Charts : a 3D chart library for the Java(tm) platform 003 * =========================================================== 004 * 005 * (C)opyright 2013-2022, by David Gilbert. All rights reserved. 006 * 007 * https://github.com/jfree/orson-charts 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.graphics3d.swing; 034 035import java.awt.BorderLayout; 036import java.awt.Dimension; 037import java.awt.Graphics; 038import java.awt.Graphics2D; 039import java.awt.Insets; 040import java.awt.Point; 041import java.awt.Rectangle; 042import java.awt.geom.AffineTransform; 043import java.awt.event.MouseEvent; 044import java.awt.event.MouseListener; 045import java.awt.event.MouseMotionListener; 046import java.awt.event.MouseWheelEvent; 047import java.awt.event.MouseWheelListener; 048import java.awt.geom.Dimension2D; 049import java.io.File; 050 051import javax.swing.JPanel; 052import javax.swing.ToolTipManager; 053 054import org.jfree.chart3d.internal.Args; 055import org.jfree.chart3d.export.ExportUtils; 056import org.jfree.chart3d.graphics3d.Dimension3D; 057import org.jfree.chart3d.graphics3d.Drawable3D; 058import org.jfree.chart3d.graphics3d.Offset2D; 059import org.jfree.chart3d.graphics3d.RenderingInfo; 060import org.jfree.chart3d.graphics3d.ViewPoint3D; 061 062/** 063 * A panel that displays a set of 3D objects from a particular viewing point. 064 * The view point is maintained by the {@link Drawable3D} but the panel 065 * provides convenience methods to get/set it. 066 * <br><br> 067 * NOTE: This class is serializable, but the serialization format is subject 068 * to change in future releases and should not be relied upon for persisting 069 * instances of this class. 070 */ 071@SuppressWarnings("serial") 072public class Panel3D extends JPanel implements MouseListener, 073 MouseMotionListener, MouseWheelListener { 074 075 /** 076 * The object that is displayed in the panel. 077 */ 078 private final Drawable3D drawable; 079 080 /** 081 * The minimum viewing distance (zooming in will not go closer than this). 082 */ 083 private final double minViewingDistance; 084 085 private double maxViewingDistanceMultiplier; 086 087 /** 088 * The margin to leave around the edges of the chart when zooming to fit. 089 * This is expressed as a percentage (0.25 = 25 percent) of the width 090 * and height. 091 */ 092 private double margin; 093 094 /** The angle increment for panning left and right (in radians). */ 095 private double panIncrement; 096 097 /** The angle increment for rotating up and down (in radians). */ 098 private double rotateIncrement; 099 100 /** The roll increment (in radians). */ 101 private double rollIncrement; 102 103 /** 104 * The (screen) point of the last mouse click (will be {@code null} 105 * initially). Used to calculate the mouse drag distance and direction. 106 */ 107 private Point lastClickPoint; 108 109 /** 110 * The (screen) point of the last mouse move point that was handled. 111 */ 112 private Point lastMovePoint; 113 114 /** 115 * Temporary state to track the 2D offset during an ALT-mouse-drag 116 * operation. 117 */ 118 private Offset2D offsetAtMousePressed; 119 120 private RenderingInfo renderingInfo; 121 122 /** 123 * Creates a new panel with the specified {@link Drawable3D} to 124 * display. 125 * 126 * @param drawable the content to display ({@code null} not 127 * permitted). 128 */ 129 public Panel3D(Drawable3D drawable) { 130 super(new BorderLayout()); 131 Args.nullNotPermitted(drawable, "drawable"); 132 this.drawable = drawable; 133 this.margin = 0.25; 134 this.minViewingDistance 135 = drawable.getDimensions().getDiagonalLength(); 136 this.maxViewingDistanceMultiplier = 8.0; 137 this.panIncrement = Math.PI / 60; 138 this.rotateIncrement = Math.PI / 60; 139 this.rollIncrement = Math.PI / 60; 140 addMouseListener(this); 141 addMouseMotionListener(this); 142 addMouseWheelListener(this); 143 } 144 145 /** 146 * Returns the {@code Drawable3D} object that is displayed in this 147 * panel. This is specified via the panel constructor and there is no 148 * setter method to change it. 149 * 150 * @return The {@code Drawable3D} object (never {@code null}). 151 */ 152 public Drawable3D getDrawable() { 153 return this.drawable; 154 } 155 156 /** 157 * Returns the margin, expressed as a percentage, that controls the amount 158 * of space to leave around the edges of the 3D content when the 159 * {@code zoomToFit()} method is called. The default value is 160 * {@code 0.25} (25 percent). 161 * 162 * @return The margin. 163 */ 164 public double getMargin() { 165 return this.margin; 166 } 167 168 /** 169 * Sets the margin that controls the amount of space to leave around the 170 * edges of the 3D content when the {@code zoomToFit()} method is 171 * called. 172 * 173 * @param margin the margin (as a percentage, where 0.25 = 25 percent). 174 */ 175 public void setMargin(double margin) { 176 this.margin = margin; 177 } 178 179 /** 180 * Returns the minimum viewing distance. Zooming by mouse wheel or other 181 * means will not move the viewing point closer than this. The value 182 * is computed in the constructor from the dimensions of the drawable 183 * object. 184 * 185 * @return The minimum viewing distance. 186 */ 187 public double getMinViewingDistance() { 188 return this.minViewingDistance; 189 } 190 191 /** 192 * Returns the multiplier for the maximum viewing distance (a multiple of 193 * the minimum viewing distance). The default value is {@code 8.0}. 194 * 195 * @return The multiplier. 196 * 197 * @since 1.3 198 */ 199 public double getMaxViewingDistanceMultiplier() { 200 return this.maxViewingDistanceMultiplier; 201 } 202 203 /** 204 * Sets the multiplier used to calculate the maximum viewing distance. 205 * 206 * @param multiplier the new multiplier. 207 * 208 * @since 1.3 209 */ 210 public void setMaxViewingDistanceMultiplier(double multiplier) { 211 this.maxViewingDistanceMultiplier = multiplier; 212 } 213 214 /** 215 * Returns the angle delta for each pan left or right. The default 216 * value is {@code Math.PI / 60}. 217 * 218 * @return The angle delta (in radians). 219 */ 220 public double getPanIncrement() { 221 return panIncrement; 222 } 223 224 /** 225 * Sets the standard increment for panning left and right (a rotation 226 * specified in radians). 227 * 228 * @param panIncrement the increment (in radians). 229 */ 230 public void setPanIncrement(double panIncrement) { 231 this.panIncrement = panIncrement; 232 } 233 234 /** 235 * Returns the angle delta for each rotate up or down. The default 236 * value is {@code Math.PI / 60}. 237 * 238 * @return The angle delta (in radians). 239 */ 240 public double getRotateIncrement() { 241 return rotateIncrement; 242 } 243 244 /** 245 * Sets the vertical (up and down) rotation increment (in radians). 246 * 247 * @param rotateIncrement the increment (in radians). 248 */ 249 public void setRotateIncrement(double rotateIncrement) { 250 this.rotateIncrement = rotateIncrement; 251 } 252 253 /** 254 * Returns the angle delta for each roll operation. The default 255 * value is {@code Math.PI / 60}. 256 * 257 * @return The angle delta (in radians). 258 */ 259 public double getRollIncrement() { 260 return rollIncrement; 261 } 262 263 /** 264 * Sets the roll increment in radians. 265 * 266 * @param rollIncrement the increment (in radians). 267 */ 268 public void setRollIncrement(double rollIncrement) { 269 this.rollIncrement = rollIncrement; 270 } 271 272 /** 273 * Returns the view point that is maintained by the {@link Drawable3D} 274 * instance on display. 275 * 276 * @return The view point (never {@code null}). 277 */ 278 public ViewPoint3D getViewPoint() { 279 return this.drawable.getViewPoint(); 280 } 281 282 /** 283 * Sets a new view point and repaints the panel. 284 * 285 * @param vp the view point ({@code null} not permitted). 286 */ 287 public void setViewPoint(ViewPoint3D vp) { 288 Args.nullNotPermitted(vp, "vp"); 289 this.drawable.setViewPoint(vp); // 290 repaint(); 291 } 292 293 /** 294 * Returns the last click point (possibly {@code null}). 295 * 296 * @return The last click point (possibly {@code null}). 297 */ 298 protected Point getLastClickPoint() { 299 return this.lastClickPoint; 300 } 301 302 /** 303 * Returns the rendering info from the previous call to 304 * draw(). 305 * 306 * @return The rendering info (possibly {@code null}). 307 */ 308 protected RenderingInfo getRenderingInfo() { 309 return this.renderingInfo; 310 } 311 312 /** 313 * Rotates the view point around from left to right by the specified 314 * angle and repaints the 3D scene. The direction relative to the 315 * world coordinates depends on the orientation of the view point. 316 * 317 * @param angle the angle of rotation (in radians). 318 */ 319 public void panLeftRight(double angle) { 320 this.drawable.getViewPoint().panLeftRight(angle); 321 repaint(); 322 } 323 324 /** 325 * Adjusts the viewing distance so that the chart fits the current panel 326 * size. A margin is left (see {@link #getMargin()} around the edges to 327 * leave room for labels etc. 328 */ 329 public void zoomToFit() { 330 zoomToFit(getSize()); 331 } 332 333 /** 334 * Adjusts the viewing distance so that the chart fits the specified 335 * size. A margin is left (see {@link #getMargin()} around the edges to 336 * leave room for labels etc. 337 * 338 * @param size the target size ({@code null} not permitted). 339 */ 340 public void zoomToFit(Dimension2D size) { 341 int w = (int) (size.getWidth() * (1.0 - this.margin)); 342 int h = (int) (size.getHeight() * (1.0 - this.margin)); 343 Dimension2D target = new Dimension(w, h); 344 Dimension3D d3d = this.drawable.getDimensions(); 345 float distance = this.drawable.getViewPoint().optimalDistance(target, 346 d3d, this.drawable.getProjDistance()); 347 this.drawable.getViewPoint().setRho(distance); 348 repaint(); 349 } 350 351 /** 352 * Paints the panel by asking the drawable to render a 2D projection of the 353 * objects it is managing. 354 * 355 * @param g the graphics target ({@code null} not permitted, assumed to be 356 * an instance of {@code Graphics2D}). 357 */ 358 @Override 359 public void paintComponent(Graphics g) { 360 super.paintComponent(g); 361 Graphics2D g2 = (Graphics2D) g; 362 AffineTransform saved = g2.getTransform(); 363 Dimension size = getSize(); 364 Insets insets = getInsets(); 365 Rectangle drawArea = new Rectangle(insets.left, insets.top, 366 size.width - insets.left - insets.right, 367 size.height - insets.top - insets.bottom); 368 this.renderingInfo = this.drawable.draw(g2, drawArea); 369 g2.setTransform(saved); 370 } 371 372 /** 373 * Registers this component with the tool tip manager. 374 * 375 * @since 1.3 376 */ 377 public void registerForTooltips() { 378 ToolTipManager.sharedInstance().registerComponent(this); 379 } 380 381 /** 382 * Unregisters this component with the tool tip manager. 383 * 384 * @since 1.3 385 */ 386 public void unregisterForTooltips() { 387 ToolTipManager.sharedInstance().unregisterComponent(this); 388 } 389 390 /* (non-Javadoc) 391 * @see java.awt.event.MouseListener#mouseClicked(java.awt.event.MouseEvent) 392 */ 393 @Override 394 public void mouseClicked(MouseEvent e) { 395 // nothing to do 396 } 397 398 /* (non-Javadoc) 399 * @see java.awt.event.MouseListener#mouseEntered(java.awt.event.MouseEvent) 400 */ 401 @Override 402 public void mouseEntered(MouseEvent e) { 403 // nothing to do 404 } 405 406 /* (non-Javadoc) 407 * @see java.awt.event.MouseListener#mouseExited(java.awt.event.MouseEvent) 408 */ 409 @Override 410 public void mouseExited(MouseEvent e) { 411 // nothing to do 412 } 413 414 /* (non-Javadoc) 415 * @see java.awt.event.MouseListener#mousePressed(java.awt.event.MouseEvent) 416 */ 417 @Override 418 public void mousePressed(MouseEvent e) { 419 this.lastClickPoint = e.getPoint(); 420 this.lastMovePoint = this.lastClickPoint; 421 this.offsetAtMousePressed = this.drawable.getTranslate2D(); 422 } 423 424 /* (non-Javadoc) 425 * @see java.awt.event.MouseListener#mouseReleased(java.awt.event.MouseEvent) 426 */ 427 @Override 428 public void mouseReleased(MouseEvent e) { 429 // nothing to do 430 } 431 432 /* (non-Javadoc) 433 * @see java.awt.event.MouseMotionListener#mouseDragged(java.awt.event.MouseEvent) 434 */ 435 @Override 436 public void mouseDragged(MouseEvent e) { 437 if (e.isAltDown()) { 438 Point currPt = e.getPoint(); 439 Offset2D offset = this.offsetAtMousePressed; 440 Point lastPt = getLastClickPoint(); 441 double dx = offset.getDX() + (currPt.x - lastPt.x); 442 double dy = offset.getDY() + (currPt.y - lastPt.y); 443 this.drawable.setTranslate2D(new Offset2D(dx, dy)); 444 } else { 445 Point currPt = e.getPoint(); 446 int dx = currPt.x - this.lastMovePoint.x; 447 int dy = currPt.y - this.lastMovePoint.y; 448 this.lastMovePoint = currPt; 449 this.drawable.getViewPoint().panLeftRight(-dx * Math.PI / 120); 450 this.drawable.getViewPoint().moveUpDown(-dy * Math.PI / 120); 451 repaint(); 452 } 453 } 454 455 /* (non-Javadoc) 456 * @see java.awt.event.MouseMotionListener#mouseMoved(java.awt.event.MouseEvent) 457 */ 458 @Override 459 public void mouseMoved(MouseEvent e) { 460 // nothing to do 461 } 462 463 /** 464 * Receives notification of a mouse wheel movement and responds by moving 465 * the viewpoint in or out (zooming). 466 * 467 * @param mwe the mouse wheel event. 468 */ 469 @Override 470 public void mouseWheelMoved(MouseWheelEvent mwe) { 471 float units = mwe.getUnitsToScroll(); 472 double maxViewingDistance = this.maxViewingDistanceMultiplier 473 * this.minViewingDistance; 474 double valRho = Math.max(this.minViewingDistance, 475 Math.min(maxViewingDistance, 476 this.drawable.getViewPoint().getRho() + units)); 477 this.drawable.getViewPoint().setRho(valRho); 478 repaint(); 479 } 480 481 /** 482 * Writes the current content to the specified file in PDF format. This 483 * will only work when the OrsonPDF library is found on the classpath. 484 * Reflection is used to ensure there is no compile-time dependency on 485 * OrsonPDF (which is non-free software). 486 * 487 * @param file the output file ({@code null} not permitted). 488 * @param w the chart width. 489 * @param h the chart height. 490 * 491 * @deprecated Use ExportUtils.writeAsPDF() directly. 492 */ 493 void writeAsPDF(File file, int w, int h) { 494 ExportUtils.writeAsPDF(drawable, w, h, file); 495 } 496 497 /** 498 * Writes the current content to the specified file in SVG format. This 499 * will only work when the JFreeSVG library is found on the classpath. 500 * Reflection is used to ensure there is no compile-time dependency on 501 * JFreeSVG. 502 * 503 * @param file the output file ({@code null} not permitted). 504 * @param w the chart width. 505 * @param h the chart height. 506 * 507 * @deprecated Use ExportUtils.writeAsPDF() directly. 508 */ 509 void writeAsSVG(File file, int w, int h) { 510 ExportUtils.writeAsSVG(this.drawable, w, h, file); 511 } 512 513}