001/* ===========================================================
002 * JFreeChart : a free chart library for the Java(tm) platform
003 * ===========================================================
004 *
005 * (C) Copyright 2000-2014, by Object Refinery Limited and Contributors.
006 *
007 * Project Info:  http://www.jfree.org/jfreechart/index.html
008 *
009 * This library is free software; you can redistribute it and/or modify it
010 * under the terms of the GNU Lesser General Public License as published by
011 * the Free Software Foundation; either version 2.1 of the License, or
012 * (at your option) any later version.
013 *
014 * This library is distributed in the hope that it will be useful, but
015 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
016 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
017 * License for more details.
018 *
019 * You should have received a copy of the GNU Lesser General Public
020 * License along with this library; if not, write to the Free Software
021 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301,
022 * USA.
023 *
024 * [Oracle and Java are registered trademarks of Oracle and/or its affiliates. 
025 * Other names may be trademarks of their respective owners.]
026 *
027 * ----------------
028 * ChartCanvas.java
029 * ----------------
030 * (C) Copyright 2014, by Object Refinery Limited and Contributors.
031 *
032 * Original Author:  David Gilbert (for Object Refinery Limited);
033 * Contributor(s):   -;
034 *
035 * Changes:
036 * --------
037 * 25-Jun-2014 : Version 1 (DG);
038 * 19-Jul-2014 : Add clearRect() call for each draw (DG);
039 *
040 */
041
042package org.jfree.chart.fx;
043
044import java.awt.Graphics2D;
045import java.awt.Rectangle;
046import java.awt.geom.Point2D;
047import java.awt.geom.Rectangle2D;
048import java.util.ArrayList;
049import java.util.List;
050import javafx.scene.canvas.Canvas;
051import javafx.scene.canvas.GraphicsContext;
052import javafx.scene.control.Tooltip;
053import javafx.scene.input.MouseEvent;
054import javafx.scene.input.ScrollEvent;
055import org.jfree.chart.ChartMouseEvent;
056import org.jfree.chart.ChartRenderingInfo;
057import org.jfree.chart.JFreeChart;
058import org.jfree.chart.entity.ChartEntity;
059import org.jfree.chart.event.ChartChangeEvent;
060import org.jfree.chart.event.ChartChangeListener;
061import org.jfree.chart.fx.interaction.AnchorHandlerFX;
062import org.jfree.chart.fx.interaction.DispatchHandlerFX;
063import org.jfree.chart.fx.interaction.ChartMouseEventFX;
064import org.jfree.chart.fx.interaction.ChartMouseListenerFX;
065import org.jfree.chart.fx.interaction.TooltipHandlerFX;
066import org.jfree.chart.fx.interaction.ScrollHandlerFX;
067import org.jfree.chart.fx.interaction.PanHandlerFX;
068import org.jfree.chart.fx.interaction.MouseHandlerFX;
069import org.jfree.chart.plot.PlotRenderingInfo;
070import org.jfree.chart.util.ParamChecks;
071
072/**
073 * A canvas for displaying a {@link JFreeChart} in JavaFX.  You can use the
074 * canvas directly to display charts, but usually the {@link ChartViewer}
075 * class (which embeds a canvas) is a better option.
076 * <p>
077 * The canvas installs several default mouse handlers, if you don't like the
078 * behaviour provided by these you can retrieve the handler by ID and
079 * disable or remove it (the IDs are "tooltip", "scroll", "anchor", "pan" and 
080 * "dispatch").
081 * 
082 * <p>THE API FOR THIS CLASS IS SUBJECT TO CHANGE IN FUTURE RELEASES.  This is
083 * so that we can incorporate feedback on the (new) JavaFX support in 
084 * JFreeChart.</p>
085 * 
086 * @since 1.0.18
087 */
088public class ChartCanvas extends Canvas implements ChartChangeListener {
089    
090    /** The chart being displayed in the canvas (never null). */
091    private JFreeChart chart;
092    
093    /**
094     * The graphics drawing context (will be an instance of FXGraphics2D).
095     */
096    private Graphics2D g2;
097   
098    /** 
099     * The anchor point (can be null) is usually updated to reflect the most 
100     * recent mouse click and is used during chart rendering to update 
101     * crosshairs belonging to the chart.  
102     */
103    private Point2D anchor;
104    
105    /** The chart rendering info from the most recent drawing of the chart. */
106    private ChartRenderingInfo info;
107    
108    /** The tooltip object for the canvas (can be null). */
109    private Tooltip tooltip;
110    
111    /** 
112     * A flag that controls whether or not tooltips will be generated from the
113     * chart as the mouse pointer moves over it.
114     */
115    private boolean tooltipEnabled;
116    
117    /** Storage for registered chart mouse listeners. */
118    private transient List<ChartMouseListenerFX> chartMouseListeners;
119
120    /** The current live handler (can be null). */
121    private MouseHandlerFX liveHandler;
122    
123    /** 
124     * The list of available live mouse handlers (can be empty but not null). 
125     */
126    private List<MouseHandlerFX> availableMouseHandlers;
127    
128    /** The auxiliary mouse handlers (can be empty but not null). */
129    private List<MouseHandlerFX> auxiliaryMouseHandlers;
130    
131    /**
132     * Creates a new canvas to display the supplied chart in JavaFX.
133     * 
134     * @param chart  the chart ({@code null} not permitted). 
135     */
136    public ChartCanvas(JFreeChart chart) {
137        ParamChecks.nullNotPermitted(chart, "chart");
138        this.chart = chart;
139        this.chart.addChangeListener(this);
140        this.tooltip = null;
141        this.tooltipEnabled = true;
142        this.chartMouseListeners = new ArrayList<ChartMouseListenerFX>();
143        
144        widthProperty().addListener(evt -> draw());
145        heightProperty().addListener(evt -> draw());
146        this.g2 = new FXGraphics2D(getGraphicsContext2D());
147        this.liveHandler = null;
148        this.availableMouseHandlers = new ArrayList<MouseHandlerFX>();
149        
150        this.availableMouseHandlers.add(new PanHandlerFX("pan", true, false, 
151                false, false));
152 
153        this.auxiliaryMouseHandlers = new ArrayList<MouseHandlerFX>();
154        this.auxiliaryMouseHandlers.add(new TooltipHandlerFX("tooltip"));
155        this.auxiliaryMouseHandlers.add(new ScrollHandlerFX("scroll"));
156        this.auxiliaryMouseHandlers.add(new AnchorHandlerFX("anchor"));
157        this.auxiliaryMouseHandlers.add(new DispatchHandlerFX("dispatch"));
158        
159        setOnMouseMoved((MouseEvent e) -> { handleMouseMoved(e); });
160        setOnMouseClicked((MouseEvent e) -> { handleMouseClicked(e); });
161        setOnMousePressed((MouseEvent e) -> { handleMousePressed(e); });
162        setOnMouseDragged((MouseEvent e) -> { handleMouseDragged(e); });
163        setOnMouseReleased((MouseEvent e) -> { handleMouseReleased(e); });
164        setOnScroll((ScrollEvent event) -> { handleScroll(event); });
165    }
166    
167    /**
168     * Returns the chart that is being displayed by this node.
169     * 
170     * @return The chart (never {@code null}). 
171     */
172    public JFreeChart getChart() {
173        return this.chart;
174    }
175    
176    /**
177     * Sets the chart to be displayed by this node.
178     * 
179     * @param chart  the chart ({@code null} not permitted). 
180     */
181    public void setChart(JFreeChart chart) {
182        ParamChecks.nullNotPermitted(chart, "chart");
183        this.chart.removeChangeListener(this);
184        this.chart = chart;
185        this.chart.addChangeListener(this);
186        draw();
187    }
188    
189    /**
190     * Returns the rendering info from the most recent drawing of the chart.
191     * 
192     * @return The rendering info (possibly {@code null}). 
193     */
194    public ChartRenderingInfo getRenderingInfo() {
195        return this.info;
196    }
197
198    /**
199     * Returns the flag that controls whether or not tooltips are enabled.  
200     * The default value is {@code true}.  The {@link TooltipHandlerFX} 
201     * class will only update the tooltip if this flag is set to 
202     * {@code true}.
203     * 
204     * @return The flag. 
205     */
206    public boolean isTooltipEnabled() {
207        return this.tooltipEnabled;
208    }
209
210    /**
211     * Sets the flag that controls whether or not tooltips are enabled.
212     * 
213     * @param tooltipEnabled  the new flag value. 
214     */
215    public void setTooltipEnabled(boolean tooltipEnabled) {
216        this.tooltipEnabled = tooltipEnabled;
217    }
218    
219    /**
220     * Set the anchor point and forces a redraw of the chart (the anchor point
221     * is used to determine the position of the crosshairs on the chart, if
222     * they are visible).
223     * 
224     * @param anchor  the anchor ({@code null} permitted). 
225     */
226    public void setAnchor(Point2D anchor) {
227        this.anchor = anchor;
228        this.chart.setNotify(true);  // force a redraw
229    }
230
231    /**
232     * Registers a listener to receive {@link ChartMouseEvent} notifications.
233     *
234     * @param listener  the listener ({@code null} not permitted).
235     */
236    public void addChartMouseListener(ChartMouseListenerFX listener) {
237        ParamChecks.nullNotPermitted(listener, "listener");
238        this.chartMouseListeners.add(listener);
239    }
240
241    /**
242     * Removes a listener from the list of objects listening for chart mouse
243     * events.
244     *
245     * @param listener  the listener.
246     */
247    public void removeChartMouseListener(ChartMouseListenerFX listener) {
248        this.chartMouseListeners.remove(listener);
249    }
250    
251    /**
252     * Returns the mouse handler with the specified ID, or {@code null} if
253     * there is no handler with that ID.  This method will look for handlers
254     * in both the regular and auxiliary handler lists.
255     * 
256     * @param id  the ID ({@code null} not permitted).
257     * 
258     * @return The handler with the specified ID 
259     */
260    public MouseHandlerFX getMouseHandler(String id) {
261        for (MouseHandlerFX h: this.availableMouseHandlers) {
262            if (h.getID().equals(id)) {
263                return h;
264            }
265        }
266        for (MouseHandlerFX h: this.auxiliaryMouseHandlers) {
267            if (h.getID().equals(id)) {
268                return h;
269            }
270        }
271        return null;    
272    }
273    
274    /**
275     * Adds a mouse handler to the list of available handlers (handlers that
276     * are candidates to take the position of live handler).  The handler must
277     * have an ID that uniquely identifies it amongst the handlers registered
278     * with this canvas.
279     * 
280     * @param handler  the handler ({@code null} not permitted). 
281     */
282    public void addMouseHandler(MouseHandlerFX handler) {
283        if (!this.hasUniqueID(handler)) {
284            throw new IllegalArgumentException(
285                    "There is already a handler with that ID (" 
286                            + handler.getID() + ").");
287        }
288        this.availableMouseHandlers.add(handler);
289    }
290    
291    /**
292     * Removes a handler from the list of available handlers.
293     * 
294     * @param handler  the handler ({@code null} not permitted). 
295     */
296    public void removeMouseHandler(MouseHandlerFX handler) {
297        this.availableMouseHandlers.remove(handler);
298    }
299
300    /**
301     * Validates that the specified handler has an ID that uniquely identifies 
302     * it amongst the existing handlers for this canvas.
303     * 
304     * @param handler  the handler ({@code null} not permitted).
305     * 
306     * @return A boolean.
307     */
308    private boolean hasUniqueID(MouseHandlerFX handler) {
309        for (MouseHandlerFX h: this.availableMouseHandlers) {
310            if (handler.getID().equals(h.getID())) {
311                return false;
312            }
313        }
314        for (MouseHandlerFX h: this.auxiliaryMouseHandlers) {
315            if (handler.getID().equals(h.getID())) {
316                return false;
317            }
318        }
319        return true;    
320    }
321    
322    /**
323     * Clears the current live handler.  This method is intended for use by the
324     * handlers themselves, you should not call it directly.
325     */
326    public void clearLiveHandler() {
327        this.liveHandler = null;    
328    }
329    
330    /**
331     * Draws the content of the canvas and updates the 
332     * {@code renderingInfo} attribute with the latest rendering 
333     * information.
334     */
335    public final void draw() {
336        GraphicsContext ctx = getGraphicsContext2D();
337        ctx.save();
338        double width = getWidth();
339        double height = getHeight();
340        if (width > 0 && height > 0) {
341            ctx.clearRect(0, 0, width, height);
342            this.info = new ChartRenderingInfo();
343            this.chart.draw(this.g2, new Rectangle((int) width, (int) height), 
344                    this.anchor, this.info);
345        }
346        ctx.restore();
347        this.anchor = null;
348    }
349 
350    /**
351     * Returns the data area (the area inside the axes) for the plot or subplot.
352     *
353     * @param point  the selection point (for subplot selection).
354     *
355     * @return The data area.
356     */
357    public Rectangle2D findDataArea(Point2D point) {
358        PlotRenderingInfo plotInfo = this.info.getPlotInfo();
359        Rectangle2D result;
360        if (plotInfo.getSubplotCount() == 0) {
361            result = plotInfo.getDataArea();
362        }
363        else {
364            int subplotIndex = plotInfo.getSubplotIndex(point);
365            if (subplotIndex == -1) {
366                return null;
367            }
368            result = plotInfo.getSubplotInfo(subplotIndex).getDataArea();
369        }
370        return result;
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     * Sets the tooltip text, with the (x, y) location being used for the
385     * anchor.  If the text is {@code null}, no tooltip will be displayed.
386     * This method is intended for calling by the {@link TooltipHandlerFX}
387     * class, you won't normally call it directly.
388     * 
389     * @param text  the text ({@code null} permitted).
390     * @param x  the x-coordinate of the mouse pointer.
391     * @param y  the y-coordinate of the mouse pointer.
392     */
393    public void setTooltip(String text, double x, double y) {
394        if (text != null) {
395            if (this.tooltip == null) {
396                this.tooltip = new Tooltip(text);
397                Tooltip.install(this, this.tooltip);
398            } else {
399                this.tooltip.setText(text);           
400                this.tooltip.setAnchorX(x);
401                this.tooltip.setAnchorY(y);
402            }                   
403        } else {
404            Tooltip.uninstall(this, this.tooltip);
405            this.tooltip = null;
406        }
407    }
408    
409    /**
410     * Handles a mouse pressed event by (1) selecting a live handler if one
411     * is not already selected, (2) passing the event to the live handler if
412     * there is one, and (3) passing the event to all enabled auxiliary 
413     * handlers.
414     * 
415     * @param e  the mouse event.
416     */
417    private void handleMousePressed(MouseEvent e) {
418        if (this.liveHandler == null) {
419            for (MouseHandlerFX handler: this.availableMouseHandlers) {
420                if (handler.isEnabled() && handler.hasMatchingModifiers(e)) {
421                    this.liveHandler = handler;      
422                }
423            }
424        }
425        
426        if (this.liveHandler != null) {
427            this.liveHandler.handleMousePressed(this, e);
428        }
429        
430        // pass on the event to the auxiliary handlers
431        for (MouseHandlerFX handler: this.auxiliaryMouseHandlers) {
432            if (handler.isEnabled()) {
433                handler.handleMousePressed(this, e);
434            }
435        }
436    }
437    
438    /**
439     * Handles a mouse moved event by passing it on to the registered handlers.
440     * 
441     * @param e  the mouse event.
442     */
443    private void handleMouseMoved(MouseEvent e) {
444        if (this.liveHandler != null && this.liveHandler.isEnabled()) {
445            this.liveHandler.handleMouseMoved(this, e);
446        }
447        
448        for (MouseHandlerFX handler: this.auxiliaryMouseHandlers) {
449            if (handler.isEnabled()) {
450                handler.handleMouseMoved(this, e);
451            }
452        }
453    }
454
455    /**
456     * Handles a mouse dragged event by passing it on to the registered 
457     * handlers.
458     * 
459     * @param e  the mouse event.
460     */
461    private void handleMouseDragged(MouseEvent e) {
462        if (this.liveHandler != null && this.liveHandler.isEnabled()) {
463            this.liveHandler.handleMouseDragged(this, e);
464        }
465        
466        // pass on the event to the auxiliary handlers
467        for (MouseHandlerFX handler: this.auxiliaryMouseHandlers) {
468            if (handler.isEnabled()) {
469                handler.handleMouseDragged(this, e);
470            }
471        }
472    }
473
474    /**
475     * Handles a mouse released event by passing it on to the registered 
476     * handlers.
477     * 
478     * @param e  the mouse event.
479     */
480    private void handleMouseReleased(MouseEvent e) {
481        if (this.liveHandler != null && this.liveHandler.isEnabled()) {
482            this.liveHandler.handleMouseReleased(this, e);
483        }
484        
485        // pass on the event to the auxiliary handlers
486        for (MouseHandlerFX handler: this.auxiliaryMouseHandlers) {
487            if (handler.isEnabled()) {
488                handler.handleMouseReleased(this, e);
489            }
490        }
491    }
492    
493    /**
494     * Handles a mouse released event by passing it on to the registered 
495     * handlers.
496     * 
497     * @param e  the mouse event.
498     */
499    private void handleMouseClicked(MouseEvent e) {
500        if (this.liveHandler != null && this.liveHandler.isEnabled()) {
501            this.liveHandler.handleMouseClicked(this, e);
502        }
503
504        // pass on the event to the auxiliary handlers
505        for (MouseHandlerFX handler: this.auxiliaryMouseHandlers) {
506            if (handler.isEnabled()) {
507                handler.handleMouseClicked(this, e);
508            }
509        }
510    }
511
512    /**
513     * Handles a scroll event by passing it on to the registered handlers.
514     * 
515     * @param e  the scroll event.
516     */
517    protected void handleScroll(ScrollEvent e) {
518        if (this.liveHandler != null && this.liveHandler.isEnabled()) {
519            this.liveHandler.handleScroll(this, e);
520        }
521        for (MouseHandlerFX handler: this.auxiliaryMouseHandlers) {
522            if (handler.isEnabled()) {
523                handler.handleScroll(this, e);
524            }
525        }
526    }
527    
528    /**
529     * Receives a notification from the chart that it has been changed and
530     * responds by redrawing the chart entirely.
531     * 
532     * @param event  event information. 
533     */
534    @Override
535    public void chartChanged(ChartChangeEvent event) {
536        draw();
537    }
538    
539    public void dispatchMouseMovedEvent(Point2D point, MouseEvent e) {
540        double x = point.getX();
541        double y = point.getY();
542        ChartEntity entity = this.info.getEntityCollection().getEntity(x, y);
543        ChartMouseEventFX event = new ChartMouseEventFX(this.chart, e, entity);
544        for (ChartMouseListenerFX listener : this.chartMouseListeners) {
545            listener.chartMouseMoved(event);
546        }
547    }
548
549    public void dispatchMouseClickedEvent(Point2D point, MouseEvent e) {
550        double x = point.getX();
551        double y = point.getY();
552        ChartEntity entity = this.info.getEntityCollection().getEntity(x, y);
553        ChartMouseEventFX event = new ChartMouseEventFX(this.chart, e, entity);
554        for (ChartMouseListenerFX listener : this.chartMouseListeners) {
555            listener.chartMouseClicked(event);
556        }
557    }
558}
559