001/* ================================================
002 * JFreeChart-FX : JavaFX extensions for JFreeChart
003 * ================================================
004 *
005 * (C) Copyright 2017, by Object Refinery Limited and Contributors.
006 *
007 * Project Info:  https://github.com/jfree/jfreechart-fx
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-2017, by Object Refinery Limited and Contributors.
031 *
032 * Original Author:  David Gilbert (for Object Refinery Limited);
033 * Contributor(s):   -;
034 *
035 */
036
037package org.jfree.chart.fx;
038
039import java.awt.Graphics2D;
040import java.awt.Rectangle;
041import java.awt.RenderingHints;
042import java.awt.geom.Point2D;
043import java.awt.geom.Rectangle2D;
044import java.util.ArrayList;
045import java.util.List;
046import javafx.collections.FXCollections;
047import javafx.collections.ObservableList;
048import javafx.scene.canvas.Canvas;
049import javafx.scene.canvas.GraphicsContext;
050import javafx.scene.control.Tooltip;
051import javafx.scene.input.MouseEvent;
052import javafx.scene.input.ScrollEvent;
053import javafx.scene.text.FontSmoothingType;
054import org.jfree.chart.ChartMouseEvent;
055import org.jfree.chart.ChartRenderingInfo;
056import org.jfree.chart.JFreeChart;
057import org.jfree.chart.event.ChartChangeEvent;
058import org.jfree.chart.event.ChartChangeListener;
059import org.jfree.chart.event.OverlayChangeEvent;
060import org.jfree.chart.event.OverlayChangeListener;
061import org.jfree.chart.fx.interaction.AnchorHandlerFX;
062import org.jfree.chart.fx.interaction.DispatchHandlerFX;
063import org.jfree.chart.fx.interaction.ChartMouseListenerFX;
064import org.jfree.chart.fx.interaction.TooltipHandlerFX;
065import org.jfree.chart.fx.interaction.ScrollHandlerFX;
066import org.jfree.chart.fx.interaction.PanHandlerFX;
067import org.jfree.chart.fx.interaction.MouseHandlerFX;
068import org.jfree.chart.fx.overlay.OverlayFX;
069import org.jfree.chart.plot.PlotRenderingInfo;
070import org.jfree.chart.util.Args;
071import org.jfree.fx.FXGraphics2D;
072import org.jfree.fx.FXHints;
073
074/**
075 * A canvas for displaying a {@link JFreeChart} in JavaFX.  You can use the
076 * canvas directly to display charts, but usually the {@link ChartViewer}
077 * class (which embeds a canvas) is a better option as it provides additional
078 * features.
079 * <p>
080 * The canvas installs several default mouse handlers, if you don't like the
081 * behaviour provided by these you can retrieve the handler by ID and
082 * disable or remove it (the IDs are "tooltip", "scroll", "anchor", "pan" and 
083 * "dispatch").</p>
084 * <p>
085 * The {@code FontSmoothingType} for the underlying {@code GraphicsContext} is
086 * set to {@code FontSmoothingType.LCD} as this gives better results on the 
087 * systems we've tested on.  You can modify this using 
088 * {@code getGraphicsContext().setFontSmoothingType(yourValue)}.</p>
089 *
090 */
091public class ChartCanvas extends Canvas implements ChartChangeListener,
092        OverlayChangeListener {
093    
094    /** The chart being displayed in the canvas. */
095    private JFreeChart chart;
096    
097    /**
098     * The graphics drawing context (will be an instance of FXGraphics2D).
099     */
100    private Graphics2D g2;
101   
102    /** 
103     * The anchor point (can be null) is usually updated to reflect the most 
104     * recent mouse click and is used during chart rendering to update 
105     * crosshairs belonging to the chart.  
106     */
107    private Point2D anchor;
108    
109    /** The chart rendering info from the most recent drawing of the chart. */
110    private ChartRenderingInfo info;
111    
112    /** The tooltip object for the canvas (can be null). */
113    private Tooltip tooltip;
114    
115    /** 
116     * A flag that controls whether or not tooltips will be generated from the
117     * chart as the mouse pointer moves over it.
118     */
119    private boolean tooltipEnabled;
120    
121    /** Storage for registered chart mouse listeners. */
122    private transient List<ChartMouseListenerFX> chartMouseListeners;
123
124    /** The current live handler (can be null). */
125    private MouseHandlerFX liveHandler;
126    
127    /** 
128     * The list of available live mouse handlers (can be empty but not null). 
129     */
130    private List<MouseHandlerFX> availableMouseHandlers;
131    
132    /** The auxiliary mouse handlers (can be empty but not null). */
133    private List<MouseHandlerFX> auxiliaryMouseHandlers;
134    
135    private ObservableList<OverlayFX> overlays;
136    
137    /** 
138     * A flag that can be used to override the plot setting for domain (x) axis
139     * zooming. 
140     */
141    private boolean domainZoomable;
142    
143    /** 
144     * A flag that can be used to override the plot setting for range (y) axis
145     * zooming. 
146     */
147    private boolean rangeZoomable;
148    
149    /**
150     * Creates a new canvas to display the supplied chart in JavaFX.  If
151     * {@code chart} is {@code null}, a blank canvas will be displayed.
152     * 
153     * @param chart  the chart. 
154     */
155    public ChartCanvas(JFreeChart chart) {
156        this.chart = chart;
157        if (this.chart != null) {
158            this.chart.addChangeListener(this);
159        }
160        this.tooltip = null;
161        this.tooltipEnabled = true;
162        this.chartMouseListeners = new ArrayList<>();
163        
164        widthProperty().addListener(e -> draw());
165        heightProperty().addListener(e -> draw());
166        // change the default font smoothing for better results
167        GraphicsContext gc = getGraphicsContext2D();
168        gc.setFontSmoothingType(FontSmoothingType.LCD);
169        FXGraphics2D fxg2 = new FXGraphics2D(gc);
170        fxg2.setRenderingHint(FXHints.KEY_USE_FX_FONT_METRICS, true);
171        fxg2.setZeroStrokeWidth(0.1);
172        fxg2.setRenderingHint(
173                    RenderingHints.KEY_FRACTIONALMETRICS, 
174                    RenderingHints.VALUE_FRACTIONALMETRICS_ON);
175        this.g2 = fxg2;
176        this.liveHandler = null;
177        this.availableMouseHandlers = new ArrayList<>();
178        
179        this.availableMouseHandlers.add(new PanHandlerFX("pan", true, false, 
180                false, false));
181 
182        this.auxiliaryMouseHandlers = new ArrayList<>();
183        this.auxiliaryMouseHandlers.add(new TooltipHandlerFX("tooltip"));
184        this.auxiliaryMouseHandlers.add(new ScrollHandlerFX("scroll"));
185        this.domainZoomable = true;
186        this.rangeZoomable = true;
187        this.auxiliaryMouseHandlers.add(new AnchorHandlerFX("anchor"));
188        this.auxiliaryMouseHandlers.add(new DispatchHandlerFX("dispatch"));
189
190        this.overlays = FXCollections.observableArrayList();
191
192        setOnMouseMoved(e -> handleMouseMoved(e));
193        setOnMouseClicked(e -> handleMouseClicked(e));
194        setOnMousePressed(e -> handleMousePressed(e));
195        setOnMouseDragged(e -> handleMouseDragged(e));
196        setOnMouseReleased(e -> handleMouseReleased(e));
197        setOnScroll(e -> handleScroll(e));
198    }
199    
200    /**
201     * Returns the chart that is being displayed by this node.
202     * 
203     * @return The chart (possibly {@code null}). 
204     */
205    public JFreeChart getChart() {
206        return this.chart;
207    }
208    
209    /**
210     * Sets the chart to be displayed by this node.
211     * 
212     * @param chart  the chart ({@code null} permitted). 
213     */
214    public void setChart(JFreeChart chart) {
215        if (this.chart != null) {
216            this.chart.removeChangeListener(this);
217        }
218        this.chart = chart;
219        if (this.chart != null) {
220            this.chart.addChangeListener(this);
221        }
222        draw();
223    }
224    
225    /**
226     * Returns the flag that determines whether or not zooming is enabled for
227     * the domain axis.
228     *
229     * @return A boolean.
230     */
231    public boolean isDomainZoomable() {
232        return this.domainZoomable;
233    }
234    
235    /**
236     * Sets the flag that controls whether or not domain axis zooming is 
237     * enabled.  If the underlying plot does not support domain axis zooming,
238     * then setting this flag to {@code true} will have no effect.
239     * 
240     * @param zoomable  the new flag value.
241     */
242    public void setDomainZoomable(boolean zoomable) {
243        this.domainZoomable = zoomable;
244    }
245    
246    /**
247     * Returns the flag that determines whether or not zooming is enabled for
248     * the range axis.
249     *
250     * @return A boolean.
251     */
252    public boolean isRangeZoomable() {
253        return this.rangeZoomable;
254    }
255
256    /**
257     * Sets the flag that controls whether or not range axis zooming is 
258     * enabled.  If the underlying plot does not support range axis zooming,
259     * then setting this flag to {@code true} will have no effect.
260     * 
261     * @param zoomable  the new flag value.
262     */
263    public void setRangeZoomable(boolean zoomable) {
264        this.rangeZoomable = zoomable;
265    }
266
267    /**
268     * Returns the rendering info from the most recent drawing of the chart.
269     * 
270     * @return The rendering info (possibly {@code null}). 
271     */
272    public ChartRenderingInfo getRenderingInfo() {
273        return this.info;
274    }
275
276    /**
277     * Returns the flag that controls whether or not tooltips are enabled.  
278     * The default value is {@code true}.  The {@link TooltipHandlerFX} 
279     * class will only update the tooltip if this flag is set to 
280     * {@code true}.
281     * 
282     * @return The flag. 
283     */
284    public boolean isTooltipEnabled() {
285        return this.tooltipEnabled;
286    }
287
288    /**
289     * Sets the flag that controls whether or not tooltips are enabled.
290     * 
291     * @param tooltipEnabled  the new flag value. 
292     */
293    public void setTooltipEnabled(boolean tooltipEnabled) {
294        this.tooltipEnabled = tooltipEnabled;
295    }
296    
297    /**
298     * Returns the anchor point.  This is the last point on the canvas
299     * that the user clicked with the mouse, and is used during chart 
300     * rendering to determine the position of crosshairs (if visible).
301     * 
302     * @return The anchor point (possibly {@code null}).
303     */
304    public Point2D getAnchor() {
305        return this.anchor;        
306    } 
307
308    /**
309     * Set the anchor point and forces a redraw of the chart (the anchor point
310     * is used to determine the position of the crosshairs on the chart, if
311     * they are visible).
312     * 
313     * @param anchor  the anchor ({@code null} permitted). 
314     */
315    public void setAnchor(Point2D anchor) {
316        this.anchor = anchor;
317        if (this.chart != null) {
318            this.chart.setNotify(true);  // force a redraw
319        }
320    }
321
322    /**
323     * Add an overlay to the canvas.
324     *
325     * @param overlay  the overlay ({@code null} not permitted).
326     */
327    public void addOverlay(OverlayFX overlay) {
328        Args.nullNotPermitted(overlay, "overlay");
329        this.overlays.add(overlay);
330        overlay.addChangeListener(this);
331        draw();
332    }
333
334    /**
335     * Removes an overlay from the canvas.
336     *
337     * @param overlay  the overlay to remove ({@code null} not permitted).
338     */
339    public void removeOverlay(OverlayFX overlay) {
340        Args.nullNotPermitted(overlay, "overlay");
341        boolean removed = this.overlays.remove(overlay);
342        if (removed) {
343            overlay.removeChangeListener(this);
344            draw();
345        }
346    }
347
348    /**
349     * Handles a change to an overlay by repainting the chart canvas.
350     *
351     * @param event  the event.
352     */
353    @Override
354    public void overlayChanged(OverlayChangeEvent event) {
355        draw();
356    }
357
358    /**
359     * Returns a (newly created) list containing the listeners currently 
360     * registered with the canvas.
361     * 
362     * @return A list of listeners (possibly empty but never {@code null}).
363     */
364    public List<ChartMouseListenerFX> getChartMouseListeners() {
365        return new ArrayList<>(this.chartMouseListeners);
366    }
367    
368    /**
369     * Registers a listener to receive {@link ChartMouseEvent} notifications.
370     *
371     * @param listener  the listener ({@code null} not permitted).
372     */
373    public void addChartMouseListener(ChartMouseListenerFX listener) {
374        Args.nullNotPermitted(listener, "listener");
375        this.chartMouseListeners.add(listener);
376    }
377
378    /**
379     * Removes a listener from the list of objects listening for chart mouse
380     * events.
381     *
382     * @param listener  the listener.
383     */
384    public void removeChartMouseListener(ChartMouseListenerFX listener) {
385        this.chartMouseListeners.remove(listener);
386    }
387    
388    /**
389     * Returns the mouse handler with the specified ID, or {@code null} if
390     * there is no handler with that ID.  This method will look for handlers
391     * in both the regular and auxiliary handler lists.
392     * 
393     * @param id  the ID ({@code null} not permitted).
394     * 
395     * @return The handler with the specified ID 
396     */
397    public MouseHandlerFX getMouseHandler(String id) {
398        for (MouseHandlerFX h: this.availableMouseHandlers) {
399            if (h.getID().equals(id)) {
400                return h;
401            }
402        }
403        for (MouseHandlerFX h: this.auxiliaryMouseHandlers) {
404            if (h.getID().equals(id)) {
405                return h;
406            }
407        }
408        return null;    
409    }
410    
411    /**
412     * Adds a mouse handler to the list of available handlers (handlers that
413     * are candidates to take the position of live handler).  The handler must
414     * have an ID that uniquely identifies it amongst the handlers registered
415     * with this canvas.
416     * 
417     * @param handler  the handler ({@code null} not permitted). 
418     */
419    public void addMouseHandler(MouseHandlerFX handler) {
420        if (!hasUniqueID(handler)) {
421            throw new IllegalArgumentException(
422                    "There is already a handler with that ID (" 
423                            + handler.getID() + ").");
424        }
425        this.availableMouseHandlers.add(handler);
426    }
427    
428    /**
429     * Removes a handler from the list of available handlers.
430     * 
431     * @param handler  the handler ({@code null} not permitted). 
432     */
433    public void removeMouseHandler(MouseHandlerFX handler) {
434        this.availableMouseHandlers.remove(handler);
435    }
436    
437    /**
438     * Adds a handler to the list of auxiliary handlers.  The handler must
439     * have an ID that uniquely identifies it amongst the handlers registered
440     * with this canvas.
441     * 
442     * @param handler  the handler ({@code null} not permitted).
443     */
444    public void addAuxiliaryMouseHandler(MouseHandlerFX handler) {
445        if (!hasUniqueID(handler)) {
446            throw new IllegalArgumentException(
447                    "There is already a handler with that ID (" 
448                            + handler.getID() + ").");
449        }
450        this.auxiliaryMouseHandlers.add(handler);
451    }
452    
453    /**
454     * Removes a handler from the list of auxiliary handlers.
455     * 
456     * @param handler  the handler ({@code null} not permitted).
457     */
458    public void removeAuxiliaryMouseHandler(MouseHandlerFX handler) {
459        this.auxiliaryMouseHandlers.remove(handler);
460    }
461
462    /**
463     * Validates that the specified handler has an ID that uniquely identifies 
464     * it amongst the existing handlers for this canvas.
465     * 
466     * @param handler  the handler ({@code null} not permitted).
467     * 
468     * @return A boolean.
469     */
470    private boolean hasUniqueID(MouseHandlerFX handler) {
471        for (MouseHandlerFX h: this.availableMouseHandlers) {
472            if (handler.getID().equals(h.getID())) {
473                return false;
474            }
475        }
476        for (MouseHandlerFX h: this.auxiliaryMouseHandlers) {
477            if (handler.getID().equals(h.getID())) {
478                return false;
479            }
480        }
481        return true;    
482    }
483    
484    /**
485     * Clears the current live handler.  This method is intended for use by the
486     * handlers themselves, you should not call it directly.
487     */
488    public void clearLiveHandler() {
489        this.liveHandler = null;    
490    }
491    
492    /**
493     * Draws the content of the canvas and updates the 
494     * {@code renderingInfo} attribute with the latest rendering 
495     * information.
496     */
497    public final void draw() {
498        GraphicsContext ctx = getGraphicsContext2D();
499        ctx.save();
500        double width = getWidth();
501        double height = getHeight();
502        if (width > 0 && height > 0) {
503            ctx.clearRect(0, 0, width, height);
504            this.info = new ChartRenderingInfo();
505            if (this.chart != null) {
506                this.chart.draw(this.g2, new Rectangle((int) width, 
507                        (int) height), this.anchor, this.info);
508            }
509        }
510        ctx.restore();
511        for (OverlayFX overlay : this.overlays) {
512            overlay.paintOverlay(g2, this);
513        }
514        this.anchor = null;
515    }
516 
517    /**
518     * Returns the data area (the area inside the axes) for the plot or subplot.
519     *
520     * @param point  the selection point (for subplot selection).
521     *
522     * @return The data area.
523     */
524    public Rectangle2D findDataArea(Point2D point) {
525        PlotRenderingInfo plotInfo = this.info.getPlotInfo();
526        Rectangle2D result;
527        if (plotInfo.getSubplotCount() == 0) {
528            result = plotInfo.getDataArea();
529        } else {
530            int subplotIndex = plotInfo.getSubplotIndex(point);
531            if (subplotIndex == -1) {
532                return null;
533            }
534            result = plotInfo.getSubplotInfo(subplotIndex).getDataArea();
535        }
536        return result;
537    }
538    
539    /**
540     * Return {@code true} to indicate the canvas is resizable.
541     * 
542     * @return {@code true}. 
543     */
544    @Override
545    public boolean isResizable() {
546        return true;
547    }
548
549    /**
550     * Sets the tooltip text, with the (x, y) location being used for the
551     * anchor.  If the text is {@code null}, no tooltip will be displayed.
552     * This method is intended for calling by the {@link TooltipHandlerFX}
553     * class, you won't normally call it directly.
554     * 
555     * @param text  the text ({@code null} permitted).
556     * @param x  the x-coordinate of the mouse pointer.
557     * @param y  the y-coordinate of the mouse pointer.
558     */
559    public void setTooltip(String text, double x, double y) {
560        if (text != null) {
561            if (this.tooltip == null) {
562                this.tooltip = new Tooltip(text);
563                Tooltip.install(this, this.tooltip);
564            } else {
565                this.tooltip.setText(text);           
566                this.tooltip.setAnchorX(x);
567                this.tooltip.setAnchorY(y);
568            }                   
569        } else {
570            Tooltip.uninstall(this, this.tooltip);
571            this.tooltip = null;
572        }
573    }
574    
575    /**
576     * Handles a mouse pressed event by (1) selecting a live handler if one
577     * is not already selected, (2) passing the event to the live handler if
578     * there is one, and (3) passing the event to all enabled auxiliary 
579     * handlers.
580     * 
581     * @param e  the mouse event.
582     */
583    private void handleMousePressed(MouseEvent e) {
584        if (this.liveHandler == null) {
585            for (MouseHandlerFX handler: this.availableMouseHandlers) {
586                if (handler.isEnabled() && handler.hasMatchingModifiers(e)) {
587                    this.liveHandler = handler;      
588                }
589            }
590        }
591        
592        if (this.liveHandler != null) {
593            this.liveHandler.handleMousePressed(this, e);
594        }
595        
596        // pass on the event to the auxiliary handlers
597        for (MouseHandlerFX handler: this.auxiliaryMouseHandlers) {
598            if (handler.isEnabled()) {
599                handler.handleMousePressed(this, e);
600            }
601        }
602    }
603    
604    /**
605     * Handles a mouse moved event by passing it on to the registered handlers.
606     * 
607     * @param e  the mouse event.
608     */
609    private void handleMouseMoved(MouseEvent e) {
610        if (this.liveHandler != null && this.liveHandler.isEnabled()) {
611            this.liveHandler.handleMouseMoved(this, e);
612        }
613        
614        for (MouseHandlerFX handler: this.auxiliaryMouseHandlers) {
615            if (handler.isEnabled()) {
616                handler.handleMouseMoved(this, e);
617            }
618        }
619    }
620
621    /**
622     * Handles a mouse dragged event by passing it on to the registered 
623     * handlers.
624     * 
625     * @param e  the mouse event.
626     */
627    private void handleMouseDragged(MouseEvent e) {
628        if (this.liveHandler != null && this.liveHandler.isEnabled()) {
629            this.liveHandler.handleMouseDragged(this, e);
630        }
631        
632        // pass on the event to the auxiliary handlers
633        for (MouseHandlerFX handler: this.auxiliaryMouseHandlers) {
634            if (handler.isEnabled()) {
635                handler.handleMouseDragged(this, e);
636            }
637        }
638    }
639
640    /**
641     * Handles a mouse released event by passing it on to the registered 
642     * handlers.
643     * 
644     * @param e  the mouse event.
645     */
646    private void handleMouseReleased(MouseEvent e) {
647        if (this.liveHandler != null && this.liveHandler.isEnabled()) {
648            this.liveHandler.handleMouseReleased(this, e);
649        }
650        
651        // pass on the event to the auxiliary handlers
652        for (MouseHandlerFX handler: this.auxiliaryMouseHandlers) {
653            if (handler.isEnabled()) {
654                handler.handleMouseReleased(this, e);
655            }
656        }
657    }
658    
659    /**
660     * Handles a mouse released event by passing it on to the registered 
661     * handlers.
662     * 
663     * @param e  the mouse event.
664     */
665    private void handleMouseClicked(MouseEvent e) {
666        if (this.liveHandler != null && this.liveHandler.isEnabled()) {
667            this.liveHandler.handleMouseClicked(this, e);
668        }
669
670        // pass on the event to the auxiliary handlers
671        for (MouseHandlerFX handler: this.auxiliaryMouseHandlers) {
672            if (handler.isEnabled()) {
673                handler.handleMouseClicked(this, e);
674            }
675        }
676    }
677
678    /**
679     * Handles a scroll event by passing it on to the registered handlers.
680     * 
681     * @param e  the scroll event.
682     */
683    protected void handleScroll(ScrollEvent e) {
684        if (this.liveHandler != null && this.liveHandler.isEnabled()) {
685            this.liveHandler.handleScroll(this, e);
686        }
687        for (MouseHandlerFX handler: this.auxiliaryMouseHandlers) {
688            if (handler.isEnabled()) {
689                handler.handleScroll(this, e);
690            }
691        }
692    }
693    
694    /**
695     * Receives a notification from the chart that it has been changed and
696     * responds by redrawing the chart entirely.
697     * 
698     * @param event  event information. 
699     */
700    @Override
701    public void chartChanged(ChartChangeEvent event) {
702        draw();
703    }
704
705}
706