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