    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.awt.Dimension;
    036import java.awt.Graphics2D;
    037import java.awt.Point;
    038import java.awt.Rectangle;
    039import java.awt.geom.Dimension2D;
    040import javafx.scene.canvas.Canvas;
    041import javafx.scene.canvas.GraphicsContext;
    042import javafx.scene.control.Tooltip;
    043import javafx.scene.input.MouseEvent;
    044import javafx.scene.input.ScrollEvent;
    045import com.orsoncharts.Chart3D;
    046import com.orsoncharts.Chart3DChangeEvent;
    047import com.orsoncharts.Chart3DChangeListener;
    048import com.orsoncharts.data.ItemKey;
    049import com.orsoncharts.graphics3d.Dimension3D;
    050import com.orsoncharts.graphics3d.Object3D;
    051import com.orsoncharts.graphics3d.Offset2D;
    052import com.orsoncharts.graphics3d.RenderingInfo;
    053import com.orsoncharts.graphics3d.ViewPoint3D;
    054import com.orsoncharts.util.ArgChecks;
    055import org.jfree.fx.FXGraphics2D;
    056
    057/**
    058 * A canvas node for displaying a {@link Chart3D} in JavaFX.  This node
    059 * handles mouse events and tooltips but does not provide a context menu or
    060 * toolbar (these features are provided by the {@link Chart3DViewer} class.)
    061 *
    062 * @since 1.4
    063 */
    064public class Chart3DCanvas extends Canvas implements Chart3DChangeListener {
    065
    066    /** The chart being displayed in the canvas. */
    067    private Chart3D chart;
    068
    069    /**
    070     * The graphics drawing context (will be an instance of FXGraphics2D).
    071     */
    072    private Graphics2D g2;
    073
    074    /** The rendering info from the last drawing of the chart. */
    075    private RenderingInfo renderingInfo;
    076
    077    /**
    078     * The minimum viewing distance (zooming in will not go closer than this).
    079     */
    080    private double minViewingDistance;
    081
    082    /**
    083     * The multiplier for the maximum viewing distance (a multiple of the
    084     * minimum viewing distance).
    085     */
    086    private double maxViewingDistanceMultiplier;
    087
    088    /**
    089     * The margin around the chart (used when zooming to fit).
    090     */
    091    private double margin = 0.25;
    092
    093    /** The angle increment for panning left and right (in radians). */
    094    private double panIncrement = Math.PI / 120.0;
    095
    096    /** The angle increment for rotating up and down (in radians). */
    097    private double rotateIncrement = Math.PI / 120.0;
    098
    099    /**
    100     * The (screen) point of the last mouse click (will be {@code null}
    101     * initially).  Used to calculate the mouse drag distance and direction.
    102     */
    103    private Point lastClickPoint;
    104
    105    /**
    106     * The (screen) point of the last mouse move point that was handled.
    107     */
    108    private Point lastMovePoint;
    109
    110    /**
    111     * Temporary state to track the 2D offset during an ALT-mouse-drag
    112     * operation.
    113     */
    114    private Offset2D offsetAtMousePressed;
    115
    116    /** The tooltip object for the canvas. */
    117    private Tooltip tooltip;
    118
    119    /** Are tooltips enabled? */
    120    private boolean tooltipEnabled = true;
    121
    122    /** Is rotation by mouse-dragging enabled? */
    123    private boolean rotateViewEnabled = true;
    124
    125    /**
    126     * Creates a new canvas to display the supplied chart in JavaFX.
    127     *
    128     * @param chart  the chart ({@code null} not permitted).
    129     */
    130    public Chart3DCanvas(Chart3D chart) {
    131        this.chart = chart;
    132        this.minViewingDistance = chart.getDimensions().getDiagonalLength();
    133        this.maxViewingDistanceMultiplier = 8.0;
    134        widthProperty().addListener(e -> draw());
    135        heightProperty().addListener(e -> draw());
    136        this.g2 = new FXGraphics2D(getGraphicsContext2D());
    137
    138        setOnMouseMoved((MouseEvent me) -> { updateTooltip(me); });
    139        setOnMousePressed((MouseEvent me) -> {
    140            Chart3DCanvas canvas = Chart3DCanvas.this;
    141            canvas.lastClickPoint = new Point((int) me.getScreenX(),
    142                    (int) me.getScreenY());
    143            canvas.lastMovePoint = canvas.lastClickPoint;
    144        });
    145
    146        setOnMouseDragged((MouseEvent me) -> { handleMouseDragged(me); });
    147        setOnScroll((ScrollEvent event) -> { handleScroll(event); });
    148        this.chart.addChangeListener(this);
    149    }
    150
    151    /**
    152     * Returns the chart that is being displayed by this node.
    153     *
    154     * @return The chart (never {@code null}).
    155     */
    156    public Chart3D getChart() {
    157        return this.chart;
    158    }
    159
    160    /**
    161     * Sets the chart to be displayed by this node.
    162     *
    163     * @param chart  the chart ({@code null} not permitted).
    164     */
    165    public void setChart(Chart3D chart) {
    166        ArgChecks.nullNotPermitted(chart, "chart");
    167        if (this.chart != null) {
    168            this.chart.removeChangeListener(this);
    169        }
    170        this.chart = chart;
    171        this.chart.addChangeListener(this);
    172        draw();
    173    }
    174
    175    /**
    176     * Returns the margin that is used when zooming to fit.  The margin can
    177     * be used to control the amount of space around the chart (where labels
    178     * are often drawn).  The default value is 0.25 (25 percent).
    179     *
    180     * @return The margin.
    181     */
    182    public double getMargin() {
    183        return this.margin;
    184    }
    185
    186    /**
    187     * Sets the margin (note that this will not have an immediate effect, it
    188     * will only be applied on the next call to
    189     * {@link #zoomToFit(double, double)}).
    190     *
    191     * @param margin  the margin.
    192     */
    193    public void setMargin(double margin) {
    194        this.margin = margin;
    195    }
    196
    197    /**
    198     * Returns the rendering info from the most recent drawing of the chart.
    199     *
    200     * @return The rendering info (possibly {@code null}).
    201     */
    202    public RenderingInfo getRenderingInfo() {
    203        return this.renderingInfo;
    204    }
    205
    206    /**
    207     * Returns the minimum distance between the viewing point and the origin.
    208     * This is initialised in the constructor based on the chart dimensions.
    209     *
    210     * @return The minimum viewing distance.
    211     */
    212    public double getMinViewingDistance() {
    213        return this.minViewingDistance;
    214    }
    215
    216    /**
    217     * Sets the minimum between the viewing point and the origin. If the
    218     * current distance is lower than the new minimum, it will be set to this
    219     * minimum value.
    220     *
    221     * @param minViewingDistance  the minimum viewing distance.
    222     */
    223    public void setMinViewingDistance(double minViewingDistance) {
    224        this.minViewingDistance = minViewingDistance;
    225        if (this.chart.getViewPoint().getRho() < this.minViewingDistance) {
    226            this.chart.getViewPoint().setRho(this.minViewingDistance);
    227        }
    228    }
    229
    230    /**
    231     * Returns the multiplier used to calculate the maximum permitted distance
    232     * between the viewing point and the origin.  The multiplier is applied to
    233     * the minimum viewing distance.  The default value is 8.0.
    234     *
    235     * @return The multiplier.
    236     */
    237    public double getMaxViewingDistanceMultiplier() {
    238        return this.maxViewingDistanceMultiplier;
    239    }
    240
    241    /**
    242     * Sets the multiplier used to calculate the maximum viewing distance.
    243     *
    244     * @param multiplier  the multiplier (must be &gt; 1.0).
    245     */
    246    public void setMaxViewingDistanceMultiplier(double multiplier) {
    247        if (multiplier < 1.0) {
    248            throw new IllegalArgumentException(
    249                    "The 'multiplier' should be greater than 1.0.");
    250        }
    251        this.maxViewingDistanceMultiplier = multiplier;
    252        double maxDistance = this.minViewingDistance * multiplier;
    253        if (this.chart.getViewPoint().getRho() > maxDistance) {
    254            this.chart.getViewPoint().setRho(maxDistance);
    255        }
    256    }
    257
    258    /**
    259     * Returns the increment for panning left and right.  This is an angle in
    260     * radians, and the default value is {@code Math.PI / 120.0}.
    261     *
    262     * @return The panning increment.
    263     */
    264    public double getPanIncrement() {
    265        return this.panIncrement;
    266    }
    267
    268    /**
    269     * Sets the increment for panning left and right (an angle measured in
    270     * radians).
    271     *
    272     * @param increment  the angle in radians.
    273     */
    274    public void setPanIncrement(double increment) {
    275        this.panIncrement = increment;
    276    }
    277
    278    /**
    279     * Returns the increment for rotating up and down.  This is an angle in
    280     * radians, and the default value is {@code Math.PI / 120.0}.
    281     *
    282     * @return The rotate increment.
    283     */
    284    public double getRotateIncrement() {
    285        return this.rotateIncrement;
    286    }
    287
    288    /**
    289     * Sets the increment for rotating up and down (an angle measured in
    290     * radians).
    291     *
    292     * @param increment  the angle in radians.
    293     */
    294    public void setRotateIncrement(double increment) {
    295        this.rotateIncrement = increment;
    296    }
    297
    298    /**
    299     * Returns the flag that controls whether or not tooltips are enabled.
    300     *
    301     * @return The flag.
    302     */
    303    public boolean isTooltipEnabled() {
    304        return this.tooltipEnabled;
    305    }
    306
    307    /**
    308     * Sets the flag that controls whether or not tooltips are enabled.
    309     *
    310     * @param tooltipEnabled  the new flag value.
    311     */
    312    public void setTooltipEnabled(boolean tooltipEnabled) {
    313        this.tooltipEnabled = tooltipEnabled;
    314    }
    315
    316    /**
    317     * Returns a flag that controls whether or not rotation by mouse dragging
    318     * is enabled.
    319     *
    320     * @return A boolean.
    321     */
    322    public boolean isRotateViewEnabled() {
    323        return this.rotateViewEnabled;
    324    }
    325
    326    /**
    327     * Sets the flag that controls whether or not rotation by mouse dragging
    328     * is enabled.
    329     *
    330     * @param enabled  the new flag value.
    331     */
    332    public void setRotateViewEnabled(boolean enabled) {
    333        this.rotateViewEnabled = enabled;
    334    }
    335
    336    /**
    337     * Adjusts the viewing distance so that the chart fits the specified
    338     * size.  A margin is left (see {@link #getMargin()}) around the edges to
    339     * leave room for labels etc.
    340     *
    341     * @param width  the width.
    342     * @param height  the height.
    343     */
    344    public void zoomToFit(double width, double height) {
    345        int w = (int) (width * (1.0 - this.margin));
    346        int h = (int) (height * (1.0 - this.margin));
    347        Dimension2D target = new Dimension(w, h);
    348        Dimension3D d3d = this.chart.getDimensions();
    349        float distance = this.chart.getViewPoint().optimalDistance(target,
    350                d3d, this.chart.getProjDistance());
    351        this.chart.getViewPoint().setRho(distance);
    352        draw();
    353    }
    354
    355    /**
    356     * Draws the content of the canvas and updates the
    357     * {@code renderingInfo} attribute with the latest rendering
    358     * information.
    359     */
    360    public void draw() {
    361        GraphicsContext ctx = getGraphicsContext2D();
    362        ctx.save();
    363        double width = getWidth();
    364        double height = getHeight();
    365        if (width > 0 && height > 0) {
    366            ctx.clearRect(0, 0, width, height);
    367            this.renderingInfo = this.chart.draw(this.g2,
    368                    new Rectangle((int) width, (int) height));
    369        }
    370        ctx.restore();
    371    }
    372
    373    /**
    374     * Return {@code true} to indicate the canvas is resizable.
    375     *
    376     * @return {@code true}.
    377     */
    378    @Override
    379    public boolean isResizable() {
    380        return true;
    381    }
    382
    383    /**
    384     * Updates the tooltip.  This method will return without doing anything if
    385     * the {@code tooltipEnabled} flag is set to false.
    386     *
    387     * @param me  the mouse event.
    388     */
    389    protected void updateTooltip(MouseEvent me) {
    390        if (!this.tooltipEnabled || this.renderingInfo == null) {
    391            return;
    392        }
    393        Object3D object = this.renderingInfo.fetchObjectAt(me.getX(),
    394                me.getY());
    395        if (object != null) {
    396            ItemKey key = (ItemKey) object.getProperty(Object3D.ITEM_KEY);
    397            if (key != null) {
    398                String toolTipText = chart.getPlot().generateToolTipText(key);
    399                if (this.tooltip == null) {
    400                    this.tooltip = new Tooltip(toolTipText);
    401                    Tooltip.install(this, this.tooltip);
    402                } else {
    403                    this.tooltip.setText(toolTipText);
    404                    this.tooltip.setAnchorX(me.getScreenX());
    405                    this.tooltip.setAnchorY(me.getScreenY());
    406                }
    407            } else {
    408                if (this.tooltip != null) {
    409                    Tooltip.uninstall(this, this.tooltip);
    410                }
    411                this.tooltip = null;
    412            }
    413        }
    414    }
    415
    416    /**
    417     * Handles a mouse dragged event by rotating the chart (unless the
    418     * {@code rotateViewEnabled} flag is set to false, in which case this
    419     * method does nothing).
    420     *
    421     * @param event  the mouse event.
    422     */
    423    private void handleMouseDragged(MouseEvent event) {
    424        if (!this.rotateViewEnabled) {
    425            return;
    426        }
    427        Point currPt = new Point((int) event.getScreenX(),
    428                    (int) event.getScreenY());
    429        int dx = currPt.x - this.lastMovePoint.x;
    430        int dy = currPt.y - this.lastMovePoint.y;
    431        this.lastMovePoint = currPt;
    432        this.chart.getViewPoint().panLeftRight(-dx * this.panIncrement);
    433        this.chart.getViewPoint().moveUpDown(-dy * this.rotateIncrement);
    434        this.draw();
    435    }
    436
    437    private void handleScroll(ScrollEvent event) {
    438        double units = -event.getDeltaY();
    439        double maxViewingDistance = this.maxViewingDistanceMultiplier
    440                    * this.minViewingDistance;
    441        ViewPoint3D vp = this.chart.getViewPoint();
    442        double valRho = Math.max(this.minViewingDistance,
    443                Math.min(maxViewingDistance, vp.getRho() + units));
    444        vp.setRho(valRho);
    445        draw();
    446    }
    447
    448    @Override
    449    public void chartChanged(Chart3DChangeEvent event) {
    450        draw();
    451    }
    452}
    453