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.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 Chart3DCanvas canvas; 071 072 /** The context menu that will be attached to the canvas. */ 073 private 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) -> { 117 contextMenu.show(Chart3DViewer.this.getScene().getWindow(), 118 event.getScreenX(), event.getScreenY()); 119 }); 120 this.contextMenu.setOnShowing((WindowEvent event) -> { 121 Chart3DViewer viewer = Chart3DViewer.this; 122 if (viewer.canvas != null) { 123 viewer.canvas.setRotateViewEnabled(false); 124 viewer.canvas.setTooltipEnabled(false); 125 } 126 }); 127 this.contextMenu.setOnHiding((WindowEvent event) -> { 128 Chart3DViewer viewer = Chart3DViewer.this; 129 if (viewer.canvas != null) { 130 viewer.canvas.setRotateViewEnabled(true); 131 viewer.canvas.setTooltipEnabled(true); 132 } 133 }); 134 } 135 136 /** 137 * Returns the chart that is being displayed by this node. 138 * 139 * @return The chart (never {@code null}). 140 */ 141 public Chart3D getChart() { 142 return this.canvas.getChart(); 143 } 144 145 /** 146 * Sets the chart to be displayed by this node. 147 * 148 * @param chart the chart ({@code null} not permitted). 149 */ 150 public void setChart(Chart3D chart) { 151 Objects.requireNonNull(chart, "chart"); 152 this.canvas.setChart(chart); 153 } 154 155 /** 156 * Returns the canvas used within this control to display the chart. 157 * 158 * @return The canvas (never {@code null}). 159 */ 160 public Chart3DCanvas getCanvas() { 161 return this.canvas; 162 } 163 164 /** 165 * Returns the multiplier used for the zoom in and out options in the 166 * context menu. The default value is {@code 0.95}. 167 * 168 * @return The zoom multiplier. 169 */ 170 public double getZoomMultiplier() { 171 return this.zoomMultiplier; 172 } 173 174 /** 175 * Sets the zoom multiplier used for the zoom in and out options in the 176 * context menu. When zooming in, the current viewing distance will be 177 * multiplied by this value (which defaults to 0.95). When zooming out, 178 * the viewing distance is multiplied by 1 / zoomMultiplier. 179 * 180 * @param multiplier the new multiplier. 181 */ 182 public void setZoomMultiplier(double multiplier) { 183 this.zoomMultiplier = multiplier; 184 } 185 186 /** 187 * Creates the context menu. 188 * 189 * @return The context menu. 190 */ 191 private ContextMenu createContextMenu() { 192 final ContextMenu menu = new ContextMenu(); 193 MenuItem zoomIn = new MenuItem("Zoom In"); 194 zoomIn.setOnAction(e -> { handleZoom(this.zoomMultiplier); }); 195 MenuItem zoomOut = new MenuItem("Zoom Out"); 196 zoomOut.setOnAction(e -> { handleZoom(1.0 / this.zoomMultiplier); }); 197 198 MenuItem zoomToFit = new MenuItem("Zoom To Fit"); 199 zoomToFit.setOnAction(e -> { handleZoomToFit(); }); 200 201 SeparatorMenuItem separator = new SeparatorMenuItem(); 202 Menu export = new Menu("Export As"); 203 204 MenuItem pngItem = new MenuItem("PNG..."); 205 pngItem.setOnAction(e -> { handleExportToPNG(); }); 206 export.getItems().add(pngItem); 207 208 MenuItem jpegItem = new MenuItem("JPEG..."); 209 jpegItem.setOnAction(e -> { handleExportToJPEG(); }); 210 export.getItems().add(jpegItem); 211 212 // automatically detect if OrsonPDF is on the classpath and, if it is, 213 // provide a PDF export menu item 214 if (ExportFormats.isJFreePDFAvailable()) { 215 MenuItem pdfItem = new MenuItem("PDF..."); 216 pdfItem.setOnAction(e -> { handleExportToPDF(); }); 217 export.getItems().add(pdfItem); 218 } 219 // automatically detect if JFreeSVG is on the classpath and, if it is, 220 // provide a SVG export menu item 221 if (ExportFormats.isJFreeSVGAvailable()) { 222 MenuItem svgItem = new MenuItem("SVG..."); 223 svgItem.setOnAction(e -> { handleExportToSVG(); }); 224 export.getItems().add(svgItem); 225 } 226 menu.getItems().addAll(zoomIn, zoomOut, zoomToFit, separator, export); 227 return menu; 228 } 229 230 /** 231 * A handler for the zoom in and out options in the context menu. 232 * 233 * @param multiplier the multiplier (less than 1.0 zooms in, greater than 234 * 1.0 zooms out). 235 */ 236 private void handleZoom(double multiplier) { 237 ViewPoint3D viewPt = getChart().getViewPoint(); 238 double minDistance = this.canvas.getMinViewingDistance(); 239 double maxDistance = minDistance 240 * this.canvas.getMaxViewingDistanceMultiplier(); 241 double valRho = Math.max(minDistance, 242 Math.min(maxDistance, viewPt.getRho() * multiplier)); 243 viewPt.setRho(valRho); 244 this.canvas.draw(); 245 } 246 247 /** 248 * A handler for the zoom to fit option in the context menu. 249 */ 250 private void handleZoomToFit() { 251 this.canvas.zoomToFit(canvas.getWidth(), canvas.getHeight()); 252 } 253 254 /** 255 * A handler for the export to PDF option in the context menu. Note that 256 * the Export to PDF menu item is only installed if OrsonPDF is on the 257 * classpath. 258 */ 259 private void handleExportToPDF() { 260 FileChooser fileChooser = new FileChooser(); 261 fileChooser.setSelectedExtensionFilter(new FileChooser.ExtensionFilter( 262 "Portable Document Format (PDF)", "pdf")); 263 fileChooser.setTitle("Export to PDF"); 264 File file = fileChooser.showSaveDialog(this.getScene().getWindow()); 265 if (file != null) { 266 ExportUtils.writeAsPDF(getChart(), (int) getWidth(), 267 (int) getHeight(), file); 268 } 269 } 270 271 /** 272 * A handler for the export to SVG option in the context menu. 273 */ 274 private void handleExportToSVG() { 275 FileChooser fileChooser = new FileChooser(); 276 fileChooser.setTitle("Export to SVG"); 277 fileChooser.setSelectedExtensionFilter(new FileChooser.ExtensionFilter( 278 "Scalable Vector Graphics (SVG)", "svg")); 279 File file = fileChooser.showSaveDialog(this.getScene().getWindow()); 280 if (file != null) { 281 ExportUtils.writeAsSVG(getChart(), (int) getWidth(), 282 (int) getHeight(), file); 283 } 284 } 285 286 /** 287 * A handler for the export to PNG option in the context menu. 288 */ 289 private void handleExportToPNG() { 290 FileChooser fileChooser = new FileChooser(); 291 fileChooser.setTitle("Export to PNG"); 292 fileChooser.setSelectedExtensionFilter(new FileChooser.ExtensionFilter( 293 "Portable Network Graphics (PNG)", "png")); 294 File file = fileChooser.showSaveDialog(this.getScene().getWindow()); 295 if (file != null) { 296 try { 297 ExportUtils.writeAsPNG(getChart(), (int) getWidth(), 298 (int) getHeight(), file); 299 } catch (IOException ex) { 300 // FIXME: show a dialog with the error 301 } 302 } 303 } 304 305 /** 306 * A handler for the export to JPEG option in the context menu. 307 */ 308 private void handleExportToJPEG() { 309 FileChooser fileChooser = new FileChooser(); 310 fileChooser.setTitle("Export to JPEG"); 311 fileChooser.setSelectedExtensionFilter(new FileChooser.ExtensionFilter( 312 "JPEG", "jpg")); 313 File file = fileChooser.showSaveDialog(this.getScene().getWindow()); 314 if (file != null) { 315 try { 316 ExportUtils.writeAsJPEG(getChart(), (int) getWidth(), 317 (int) getHeight(), file); 318 } catch (IOException ex) { 319 // FIXME: show a dialog with the error 320 } 321 } 322 } 323 324 /** 325 * Called by the JavaFX layout mechanism, this method aligns the underlying 326 * chart canvas with the bounds of the viewer. 327 */ 328 @Override 329 protected void layoutChildren() { 330 super.layoutChildren(); 331 this.canvas.setLayoutX(0); 332 this.canvas.setLayoutY(0); 333 this.canvas.setWidth(getWidth()); 334 this.canvas.setHeight(getHeight()); 335 } 336 337} 338