    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