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