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