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