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