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