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.io.File; 036import java.io.IOException; 037import java.util.Objects; 038import javafx.scene.control.ContextMenu; 039import javafx.scene.control.Menu; 040import javafx.scene.control.MenuItem; 041import javafx.scene.control.SeparatorMenuItem; 042import javafx.scene.input.ContextMenuEvent; 043import javafx.scene.input.MouseEvent; 044import javafx.scene.layout.Region; 045import javafx.stage.FileChooser; 046import javafx.stage.WindowEvent; 047import org.jfree.chart3d.Chart3D; 048import org.jfree.chart3d.export.ExportFormats; 049import org.jfree.chart3d.export.ExportUtils; 050import org.jfree.chart3d.fx.interaction.FXChart3DMouseEvent; 051import org.jfree.chart3d.graphics3d.RenderedElement; 052import org.jfree.chart3d.graphics3d.RenderingInfo; 053import org.jfree.chart3d.graphics3d.ViewPoint3D; 054 055/** 056 * A control for displaying a {@link Chart3D} in JavaFX. This control embeds 057 * a {@link Chart3DCanvas} and also provides a context menu. 058 */ 059public class Chart3DViewer extends Region { 060 061 private static final int DEFAULT_MIN_WIDTH = 50; 062 063 private static final int DEFAULT_MIN_HEIGHT = 50; 064 065 /** 066 * The chart canvas (which is a child node for this control). This 067 * reference is kept for convenience, and is initialised by the control's 068 * skin. 069 */ 070 private final Chart3DCanvas canvas; 071 072 /** The context menu that will be attached to the canvas. */ 073 private final ContextMenu contextMenu; 074 075 /** 076 * The zoom multiplier (applicable for the zoom in and out options in 077 * the context menu). 078 */ 079 private double zoomMultiplier = 0.95; 080 081 /** 082 * Creates a new viewer to display the supplied chart in JavaFX. 083 * 084 * @param chart the chart ({@code null} not permitted). 085 */ 086 public Chart3DViewer(Chart3D chart) { 087 this(chart, true); 088 } 089 090 /** 091 * Creates a new viewer instance. 092 * 093 * @param chart the chart ({@code null} not permitted). 094 * @param contextMenuEnabled enable the context menu? 095 */ 096 public Chart3DViewer(Chart3D chart, boolean contextMenuEnabled) { 097 Objects.requireNonNull(chart, "chart"); 098 setMinSize(DEFAULT_MIN_WIDTH, DEFAULT_MIN_HEIGHT); 099 setPrefSize(DEFAULT_MIN_WIDTH, DEFAULT_MIN_HEIGHT); 100 this.canvas = new Chart3DCanvas(chart); 101 this.canvas.addEventHandler(MouseEvent.MOUSE_CLICKED, 102 (MouseEvent event) -> { 103 RenderingInfo info = canvas.getRenderingInfo(); 104 RenderedElement element = info.findElementAt( 105 event.getX(), event.getY()); 106 107 Chart3DViewer viewer = Chart3DViewer.this; 108 viewer.fireEvent(new FXChart3DMouseEvent(viewer, 109 FXChart3DMouseEvent.MOUSE_CLICKED, element, event)); 110 }); 111 this.canvas.setTooltipEnabled(true); 112 setFocusTraversable(true); 113 getChildren().add(this.canvas); 114 115 this.contextMenu = createContextMenu(); 116 setOnContextMenuRequested((ContextMenuEvent event) -> contextMenu.show(Chart3DViewer.this.getScene().getWindow(), 117 event.getScreenX(), event.getScreenY())); 118 this.contextMenu.setOnShowing((WindowEvent event) -> { 119 Chart3DViewer viewer = Chart3DViewer.this; 120 viewer.canvas.setRotateViewEnabled(false); 121 viewer.canvas.setTooltipEnabled(false); 122 }); 123 this.contextMenu.setOnHiding((WindowEvent event) -> { 124 Chart3DViewer viewer = Chart3DViewer.this; 125 viewer.canvas.setRotateViewEnabled(true); 126 viewer.canvas.setTooltipEnabled(true); 127 }); 128 } 129 130 /** 131 * Returns the chart that is being displayed by this node. 132 * 133 * @return The chart (never {@code null}). 134 */ 135 public Chart3D getChart() { 136 return this.canvas.getChart(); 137 } 138 139 /** 140 * Sets the chart to be displayed by this node. 141 * 142 * @param chart the chart ({@code null} not permitted). 143 */ 144 public void setChart(Chart3D chart) { 145 Objects.requireNonNull(chart, "chart"); 146 this.canvas.setChart(chart); 147 } 148 149 /** 150 * Returns the canvas used within this control to display the chart. 151 * 152 * @return The canvas (never {@code null}). 153 */ 154 public Chart3DCanvas getCanvas() { 155 return this.canvas; 156 } 157 158 /** 159 * Returns the multiplier used for the zoom in and out options in the 160 * context menu. The default value is {@code 0.95}. 161 * 162 * @return The zoom multiplier. 163 */ 164 public double getZoomMultiplier() { 165 return this.zoomMultiplier; 166 } 167 168 /** 169 * Sets the zoom multiplier used for the zoom in and out options in the 170 * context menu. When zooming in, the current viewing distance will be 171 * multiplied by this value (which defaults to 0.95). When zooming out, 172 * the viewing distance is multiplied by 1 / zoomMultiplier. 173 * 174 * @param multiplier the new multiplier. 175 */ 176 public void setZoomMultiplier(double multiplier) { 177 this.zoomMultiplier = multiplier; 178 } 179 180 /** 181 * Creates the context menu. 182 * 183 * @return The context menu. 184 */ 185 private ContextMenu createContextMenu() { 186 final ContextMenu menu = new ContextMenu(); 187 MenuItem zoomIn = new MenuItem("Zoom In"); 188 zoomIn.setOnAction(e -> handleZoom(this.zoomMultiplier)); 189 MenuItem zoomOut = new MenuItem("Zoom Out"); 190 zoomOut.setOnAction(e -> handleZoom(1.0 / this.zoomMultiplier)); 191 192 MenuItem zoomToFit = new MenuItem("Zoom To Fit"); 193 zoomToFit.setOnAction(e -> handleZoomToFit()); 194 195 SeparatorMenuItem separator = new SeparatorMenuItem(); 196 Menu export = new Menu("Export As"); 197 198 MenuItem pngItem = new MenuItem("PNG..."); 199 pngItem.setOnAction(e -> handleExportToPNG()); 200 export.getItems().add(pngItem); 201 202 MenuItem jpegItem = new MenuItem("JPEG..."); 203 jpegItem.setOnAction(e -> handleExportToJPEG()); 204 export.getItems().add(jpegItem); 205 206 // automatically detect if OrsonPDF is on the classpath and, if it is, 207 // provide a PDF export menu item 208 if (ExportFormats.isJFreePDFAvailable()) { 209 MenuItem pdfItem = new MenuItem("PDF..."); 210 pdfItem.setOnAction(e -> handleExportToPDF()); 211 export.getItems().add(pdfItem); 212 } 213 // automatically detect if JFreeSVG is on the classpath and, if it is, 214 // provide an SVG export menu item 215 if (ExportFormats.isJFreeSVGAvailable()) { 216 MenuItem svgItem = new MenuItem("SVG..."); 217 svgItem.setOnAction(e -> handleExportToSVG()); 218 export.getItems().add(svgItem); 219 } 220 menu.getItems().addAll(zoomIn, zoomOut, zoomToFit, separator, export); 221 return menu; 222 } 223 224 /** 225 * A handler for the zoom in and out options in the context menu. 226 * 227 * @param multiplier the multiplier (less than 1.0 zooms in, greater than 228 * 1.0 zooms out). 229 */ 230 private void handleZoom(double multiplier) { 231 ViewPoint3D viewPt = getChart().getViewPoint(); 232 double minDistance = this.canvas.getMinViewingDistance(); 233 double maxDistance = minDistance 234 * this.canvas.getMaxViewingDistanceMultiplier(); 235 double valRho = Math.max(minDistance, 236 Math.min(maxDistance, viewPt.getRho() * multiplier)); 237 viewPt.setRho(valRho); 238 this.canvas.draw(); 239 } 240 241 /** 242 * A handler for the zoom to fit option in the context menu. 243 */ 244 private void handleZoomToFit() { 245 this.canvas.zoomToFit(canvas.getWidth(), canvas.getHeight()); 246 } 247 248 /** 249 * A handler for the export to PDF option in the context menu. Note that 250 * the Export to PDF menu item is only installed if OrsonPDF is on the 251 * classpath. 252 */ 253 private void handleExportToPDF() { 254 FileChooser fileChooser = new FileChooser(); 255 fileChooser.setSelectedExtensionFilter(new FileChooser.ExtensionFilter( 256 "Portable Document Format (PDF)", "pdf")); 257 fileChooser.setTitle("Export to PDF"); 258 File file = fileChooser.showSaveDialog(this.getScene().getWindow()); 259 if (file != null) { 260 ExportUtils.writeAsPDF(getChart(), (int) getWidth(), 261 (int) getHeight(), file); 262 } 263 } 264 265 /** 266 * A handler for the export to SVG option in the context menu. 267 */ 268 private void handleExportToSVG() { 269 FileChooser fileChooser = new FileChooser(); 270 fileChooser.setTitle("Export to SVG"); 271 fileChooser.setSelectedExtensionFilter(new FileChooser.ExtensionFilter( 272 "Scalable Vector Graphics (SVG)", "svg")); 273 File file = fileChooser.showSaveDialog(this.getScene().getWindow()); 274 if (file != null) { 275 ExportUtils.writeAsSVG(getChart(), (int) getWidth(), 276 (int) getHeight(), file); 277 } 278 } 279 280 /** 281 * A handler for the export to PNG option in the context menu. 282 */ 283 private void handleExportToPNG() { 284 FileChooser fileChooser = new FileChooser(); 285 fileChooser.setTitle("Export to PNG"); 286 fileChooser.setSelectedExtensionFilter(new FileChooser.ExtensionFilter( 287 "Portable Network Graphics (PNG)", "png")); 288 File file = fileChooser.showSaveDialog(this.getScene().getWindow()); 289 if (file != null) { 290 try { 291 ExportUtils.writeAsPNG(getChart(), (int) getWidth(), 292 (int) getHeight(), file); 293 } catch (IOException ex) { 294 // FIXME: show a dialog with the error 295 } 296 } 297 } 298 299 /** 300 * A handler for the export to JPEG option in the context menu. 301 */ 302 private void handleExportToJPEG() { 303 FileChooser fileChooser = new FileChooser(); 304 fileChooser.setTitle("Export to JPEG"); 305 fileChooser.setSelectedExtensionFilter(new FileChooser.ExtensionFilter( 306 "JPEG", "jpg")); 307 File file = fileChooser.showSaveDialog(this.getScene().getWindow()); 308 if (file != null) { 309 try { 310 ExportUtils.writeAsJPEG(getChart(), (int) getWidth(), 311 (int) getHeight(), file); 312 } catch (IOException ex) { 313 // FIXME: show a dialog with the error 314 } 315 } 316 } 317 318 /** 319 * Called by the JavaFX layout mechanism, this method aligns the underlying 320 * chart canvas with the bounds of the viewer. 321 */ 322 @Override 323 protected void layoutChildren() { 324 super.layoutChildren(); 325 this.canvas.setLayoutX(0); 326 this.canvas.setLayoutY(0); 327 this.canvas.setWidth(getWidth()); 328 this.canvas.setHeight(getHeight()); 329 } 330 331} 332