001/* ===========================================================
002 * JFreeChart : a free chart library for the Java(tm) platform
003 * ===========================================================
004 *
005 * (C) Copyright 2000-2022, by David Gilbert 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 * ChartPanel.java
029 * ---------------
030 * (C) Copyright 2000-2022, by David Gilbert and Contributors.
031 *
032 * Original Author:  David Gilbert;
033 * Contributor(s):   Andrzej Porebski;
034 *                   Soren Caspersen;
035 *                   Jonathan Nash;
036 *                   Hans-Jurgen Greiner;
037 *                   Andreas Schneider;
038 *                   Daniel van Enckevort;
039 *                   David M O'Donnell;
040 *                   Arnaud Lelievre;
041 *                   Matthias Rose;
042 *                   Onno vd Akker;
043 *                   Sergei Ivanov;
044 *                   Ulrich Voigt - patch 2686040;
045 *                   Alessandro Borges - patch 1460845;
046 *                   Martin Hoeller;
047 *                   Simon Legner - patch from bug 1129;
048 */
049
050package org.jfree.chart.swing;
051
052import java.awt.AWTEvent;
053import java.awt.AlphaComposite;
054import java.awt.Composite;
055import java.awt.Cursor;
056import java.awt.Dimension;
057import java.awt.Graphics;
058import java.awt.Graphics2D;
059import java.awt.GraphicsConfiguration;
060import java.awt.Image;
061import java.awt.Insets;
062import java.awt.Paint;
063import java.awt.Point;
064import java.awt.Rectangle;
065import java.awt.Toolkit;
066import java.awt.Transparency;
067import java.awt.datatransfer.Clipboard;
068import java.awt.event.ActionEvent;
069import java.awt.event.ActionListener;
070import java.awt.event.InputEvent;
071import java.awt.event.MouseEvent;
072import java.awt.event.MouseListener;
073import java.awt.event.MouseMotionListener;
074import java.awt.geom.AffineTransform;
075import java.awt.geom.Point2D;
076import java.awt.geom.Rectangle2D;
077import java.awt.print.PageFormat;
078import java.awt.print.Printable;
079import java.awt.print.PrinterException;
080import java.awt.print.PrinterJob;
081import java.io.BufferedWriter;
082import java.io.File;
083import java.io.FileWriter;
084import java.io.IOException;
085import java.io.ObjectInputStream;
086import java.io.ObjectOutputStream;
087import java.io.Serializable;
088import java.lang.reflect.Constructor;
089import java.lang.reflect.InvocationTargetException;
090import java.lang.reflect.Method;
091import java.util.ArrayList;
092import java.util.EventListener;
093import java.util.HashMap;
094import java.util.List;
095import java.util.Map;
096import java.util.ResourceBundle;
097
098import javax.swing.JFileChooser;
099import javax.swing.JMenu;
100import javax.swing.JMenuItem;
101import javax.swing.JOptionPane;
102import javax.swing.JPanel;
103import javax.swing.JPopupMenu;
104import javax.swing.SwingUtilities;
105import javax.swing.ToolTipManager;
106import javax.swing.event.EventListenerList;
107import javax.swing.filechooser.FileNameExtensionFilter;
108import org.jfree.chart.ChartRenderingInfo;
109import org.jfree.chart.ChartTransferable;
110import org.jfree.chart.ChartUtils;
111import org.jfree.chart.JFreeChart;
112
113import org.jfree.chart.swing.editor.ChartEditor;
114import org.jfree.chart.swing.editor.ChartEditorManager;
115import org.jfree.chart.entity.ChartEntity;
116import org.jfree.chart.entity.EntityCollection;
117import org.jfree.chart.event.ChartChangeEvent;
118import org.jfree.chart.event.ChartChangeListener;
119import org.jfree.chart.event.ChartProgressEvent;
120import org.jfree.chart.event.ChartProgressListener;
121import org.jfree.chart.plot.Pannable;
122import org.jfree.chart.plot.Plot;
123import org.jfree.chart.plot.PlotOrientation;
124import org.jfree.chart.plot.PlotRenderingInfo;
125import org.jfree.chart.plot.Zoomable;
126import org.jfree.chart.internal.Args;
127
128/**
129 * A Swing GUI component for displaying a {@link JFreeChart} object.
130 * <P>
131 * The panel registers with the chart to receive notification of changes to any
132 * component of the chart.  The chart is redrawn automatically whenever this
133 * notification is received.
134 */
135@SuppressWarnings("unused")
136public class ChartPanel extends JPanel implements ChartChangeListener,
137        ChartProgressListener, ActionListener, MouseListener,
138        MouseMotionListener, OverlayChangeListener, Printable, Serializable {
139
140    /** For serialization. */
141    protected static final long serialVersionUID = 6046366297214274674L;
142
143    /**
144     * Default setting for buffer usage.  The default has been changed to
145     * {@code true} from version 1.0.13 onwards, because of a severe
146     * performance problem with drawing the zoom rectangle using XOR (which
147     * now happens only when the buffer is NOT used).
148     */
149    public static final boolean DEFAULT_BUFFER_USED = true;
150
151    /** The default panel width. */
152    public static final int DEFAULT_WIDTH = 1024;
153
154    /** The default panel height. */
155    public static final int DEFAULT_HEIGHT = 768;
156
157    /** The default limit below which chart scaling kicks in. */
158    public static final int DEFAULT_MINIMUM_DRAW_WIDTH = 300;
159
160    /** The default limit below which chart scaling kicks in. */
161    public static final int DEFAULT_MINIMUM_DRAW_HEIGHT = 200;
162
163    /** The default limit above which chart scaling kicks in. */
164    public static final int DEFAULT_MAXIMUM_DRAW_WIDTH = 1024;
165
166    /** The default limit above which chart scaling kicks in. */
167    public static final int DEFAULT_MAXIMUM_DRAW_HEIGHT = 768;
168
169    /** Properties action command. */
170    public static final String PROPERTIES_COMMAND = "PROPERTIES";
171
172    /**
173     * Copy action command.
174     */
175    public static final String COPY_COMMAND = "COPY";
176
177    /** Save action command. */
178    public static final String SAVE_COMMAND = "SAVE";
179
180    /** Action command to save as PNG. */
181    protected static final String SAVE_AS_PNG_COMMAND = "SAVE_AS_PNG";
182    
183    /** Action command to save as PNG - use screen size */
184    protected static final String SAVE_AS_PNG_SIZE_COMMAND = "SAVE_AS_PNG_SIZE";
185
186    /** Action command to save as SVG. */
187    protected static final String SAVE_AS_SVG_COMMAND = "SAVE_AS_SVG";
188    
189    /** Action command to save as PDF. */
190    protected static final String SAVE_AS_PDF_COMMAND = "SAVE_AS_PDF";
191    
192    /** Print action command. */
193    public static final String PRINT_COMMAND = "PRINT";
194
195    /** Zoom in (both axes) action command. */
196    public static final String ZOOM_IN_BOTH_COMMAND = "ZOOM_IN_BOTH";
197
198    /** Zoom in (domain axis only) action command. */
199    public static final String ZOOM_IN_DOMAIN_COMMAND = "ZOOM_IN_DOMAIN";
200
201    /** Zoom in (range axis only) action command. */
202    public static final String ZOOM_IN_RANGE_COMMAND = "ZOOM_IN_RANGE";
203
204    /** Zoom out (both axes) action command. */
205    public static final String ZOOM_OUT_BOTH_COMMAND = "ZOOM_OUT_BOTH";
206
207    /** Zoom out (domain axis only) action command. */
208    public static final String ZOOM_OUT_DOMAIN_COMMAND = "ZOOM_DOMAIN_BOTH";
209
210    /** Zoom out (range axis only) action command. */
211    public static final String ZOOM_OUT_RANGE_COMMAND = "ZOOM_RANGE_BOTH";
212
213    /** Zoom reset (both axes) action command. */
214    public static final String ZOOM_RESET_BOTH_COMMAND = "ZOOM_RESET_BOTH";
215
216    /** Zoom reset (domain axis only) action command. */
217    public static final String ZOOM_RESET_DOMAIN_COMMAND = "ZOOM_RESET_DOMAIN";
218
219    /** Zoom reset (range axis only) action command. */
220    public static final String ZOOM_RESET_RANGE_COMMAND = "ZOOM_RESET_RANGE";
221
222    // default modifiers for zooming, private to avoid constant inlining,
223    // publicly available through getDefaultDragModifiersEx()
224    private static final int DEFAULT_DRAG_MODIFIERS_EX;
225
226    // mask for all modifier keys to check for
227    private static final int MODIFIERS_EX_MASK =
228            InputEvent.SHIFT_DOWN_MASK |
229            InputEvent.CTRL_DOWN_MASK |
230            InputEvent.META_DOWN_MASK |
231            InputEvent.ALT_DOWN_MASK;
232
233    static {
234        int dragModifiers = InputEvent.CTRL_DOWN_MASK;
235        // for MacOSX we can't use the CTRL key for mouse drags, see:
236        // http://developer.apple.com/qa/qa2004/qa1362.html
237        String osName = System.getProperty("os.name").toLowerCase();
238        if (osName.startsWith("mac os x")) {
239            dragModifiers = InputEvent.ALT_DOWN_MASK;
240        }
241        DEFAULT_DRAG_MODIFIERS_EX = dragModifiers;
242    }
243
244    /**
245     * The standard mouse button modifiers for alternative drag operations.
246     * There are two kinds of mouse drag operations: pan and zoom.
247     * To distinguish between them, one needs to require modifier keys
248     * to be held down during the dragging.  However, some modifiers
249     * may not be usable on all platforms.  For example, on Mac OS X
250     * it is impossible to perform Ctrl-drags or right-drags, see
251     * <a href="http://developer.apple.com/qa/qa2004/qa1362.html">http://developer.apple.com/qa/qa2004/qa1362.html</a>.
252     * This function returns a non-zero modifier usable for any platform:
253     * Alt for Mac OS X, Ctrl for other platforms.  It is recommended
254     * to use these modifiers for one operation, and zero modifiers for
255     * the other.
256     *
257     * @return modifiers mask, as in {@link InputEvent#getModifiersEx()}
258     * @see #setPanModifiersEx(int, int)
259     * @see #setZoomModifiersEx(int, int)
260     * @see #setDefaultPanModifiersEx(int)
261     * @see #setDefaultZoomModifiersEx(int)
262     */
263    public static int getDefaultDragModifiersEx() {
264        return DEFAULT_DRAG_MODIFIERS_EX;
265    }
266
267    /** The chart that is displayed in the panel. */
268    protected JFreeChart chart;
269
270    /** Storage for registered (chart) mouse listeners. */
271    protected transient EventListenerList chartMouseListeners;
272
273    /** A flag that controls whether or not the off-screen buffer is used. */
274    protected boolean useBuffer;
275
276    /** A flag that indicates that the buffer should be refreshed. */
277    protected boolean refreshBuffer;
278
279    /** A buffer for the rendered chart. */
280    protected transient Image chartBuffer;
281
282    /** The height of the chart buffer. */
283    protected int chartBufferHeight;
284
285    /** The width of the chart buffer. */
286    protected int chartBufferWidth;
287
288    /**
289     * The minimum width for drawing a chart (uses scaling for smaller widths).
290     */
291    protected int minimumDrawWidth;
292
293    /**
294     * The minimum height for drawing a chart (uses scaling for smaller
295     * heights).
296     */
297    protected int minimumDrawHeight;
298
299    /**
300     * The maximum width for drawing a chart (uses scaling for bigger
301     * widths).
302     */
303    protected int maximumDrawWidth;
304
305    /**
306     * The maximum height for drawing a chart (uses scaling for bigger
307     * heights).
308     */
309    protected int maximumDrawHeight;
310
311    /** The popup menu for the frame. */
312    protected JPopupMenu popup;
313
314    /** The drawing info collected the last time the chart was drawn. */
315    protected ChartRenderingInfo info;
316
317    /** The chart anchor point. */
318    protected Point2D anchor;
319
320    /** The scale factor used to draw the chart. */
321    protected double scaleX;
322
323    /** The scale factor used to draw the chart. */
324    protected double scaleY;
325
326    /** The plot orientation. */
327    protected PlotOrientation orientation = PlotOrientation.VERTICAL;
328
329    /** A flag that controls whether or not domain zooming is enabled. */
330    protected boolean domainZoomable = false;
331
332    /** A flag that controls whether or not range zooming is enabled. */
333    protected boolean rangeZoomable = false;
334
335    /** A strategy to handle zoom rectangle processing and painting. */
336    private SelectionZoomStrategy selectionZoomStrategy = new DefaultSelectionZoomStrategy();
337
338    /** Menu item for zooming in on a chart (both axes). */
339    protected JMenuItem zoomInBothMenuItem;
340
341    /** Menu item for zooming in on a chart (domain axis). */
342    protected JMenuItem zoomInDomainMenuItem;
343
344    /** Menu item for zooming in on a chart (range axis). */
345    protected JMenuItem zoomInRangeMenuItem;
346
347    /** Menu item for zooming out on a chart. */
348    protected JMenuItem zoomOutBothMenuItem;
349
350    /** Menu item for zooming out on a chart (domain axis). */
351    protected JMenuItem zoomOutDomainMenuItem;
352
353    /** Menu item for zooming out on a chart (range axis). */
354    protected JMenuItem zoomOutRangeMenuItem;
355
356    /** Menu item for resetting the zoom (both axes). */
357    protected JMenuItem zoomResetBothMenuItem;
358
359    /** Menu item for resetting the zoom (domain axis only). */
360    protected JMenuItem zoomResetDomainMenuItem;
361
362    /** Menu item for resetting the zoom (range axis only). */
363    protected JMenuItem zoomResetRangeMenuItem;
364
365    /**
366     * The default directory for saving charts to file.
367     */
368    protected File defaultDirectoryForSaveAs;
369
370    /** A flag that controls whether or not file extensions are enforced. */
371    protected boolean enforceFileExtensions;
372
373    /** A flag that indicates if original tooltip delays are changed. */
374    protected boolean ownToolTipDelaysActive;
375
376    /** Original initial tooltip delay of ToolTipManager.sharedInstance(). */
377    protected int originalToolTipInitialDelay;
378
379    /** Original reshow tooltip delay of ToolTipManager.sharedInstance(). */
380    protected int originalToolTipReshowDelay;
381
382    /** Original dismiss tooltip delay of ToolTipManager.sharedInstance(). */
383    protected int originalToolTipDismissDelay;
384
385    /** Own initial tooltip delay to be used in this chart panel. */
386    protected int ownToolTipInitialDelay;
387
388    /** Own reshow tooltip delay to be used in this chart panel. */
389    protected int ownToolTipReshowDelay;
390
391    /** Own dismiss tooltip delay to be used in this chart panel. */
392    protected int ownToolTipDismissDelay;
393
394    /** The factor used to zoom in on an axis range. */
395    protected double zoomInFactor = 0.5;
396
397    /** The factor used to zoom out on an axis range. */
398    protected double zoomOutFactor = 2.0;
399
400    /**
401     * A flag that controls whether zoom operations are centred on the
402     * current anchor point, or the centre point of the relevant axis.
403     */
404    protected boolean zoomAroundAnchor;
405
406    /** The resourceBundle for the localization. */
407    protected static ResourceBundle localizationResources
408            = ResourceBundle.getBundle("org.jfree.chart.LocalizationBundle");
409
410    /** 
411     * Temporary storage for the width and height of the chart 
412     * drawing area during panning.
413     */
414    protected double panW, panH;
415
416    /** The last mouse position during panning. */
417    protected Point panLast;
418
419    /**
420     * The default mask for mouse events to trigger panning.
421     * Since 2.0.0, this mask uses extended modifiers, as returned
422     * by {@link InputEvent#getModifiersEx()}.
423     * Only used if no button-specific modifiers were set in
424     * {@link #panButtonMasks}.
425     */
426    protected int panMask = getDefaultDragModifiersEx();
427
428    /**
429     * The default mask for mouse events to trigger zooming.
430     *
431     * @since 2.0.0
432     */
433    protected int zoomMask = 0;
434
435    /**
436     * The masks for mouse events to trigger panning, per mouse button.
437     *
438     * @since 2.0.0
439     */
440    protected final Map<Integer, Integer> panButtonMasks = new HashMap<>(3);
441
442    /**
443     * The masks for mouse events to trigger zooming, per mouse button.
444     *
445     * @since 2.0.0
446     */
447    protected final Map<Integer, Integer> zoomButtonMasks = new HashMap<>(3);
448
449    /**
450     * A list of overlays for the panel.
451     */
452    protected List<Overlay> overlays;
453    
454    /**
455     * Constructs a panel that displays the specified chart.
456     *
457     * @param chart  the chart.
458     */
459    public ChartPanel(JFreeChart chart) {
460        this(chart, DEFAULT_WIDTH, DEFAULT_HEIGHT,
461            DEFAULT_MINIMUM_DRAW_WIDTH, DEFAULT_MINIMUM_DRAW_HEIGHT,
462            DEFAULT_MAXIMUM_DRAW_WIDTH, DEFAULT_MAXIMUM_DRAW_HEIGHT,
463            DEFAULT_BUFFER_USED,
464            true,  // properties
465            true,  // save
466            true,  // print
467            true,  // zoom
468            true   // tooltips
469        );
470
471    }
472
473    /**
474     * Constructs a panel containing a chart.  The {@code useBuffer} flag
475     * controls whether or not an offscreen {@code BufferedImage} is
476     * maintained for the chart.  If the buffer is used, more memory is
477     * consumed, but panel repaints will be a lot quicker in cases where the
478     * chart itself hasn't changed (for example, when another frame is moved
479     * to reveal the panel).  WARNING: If you set the {@code useBuffer}
480     * flag to false, note that the mouse zooming rectangle will (in that case)
481     * be drawn using XOR, and there is a SEVERE performance problem with that
482     * on JRE6 on Windows.
483     *
484     * @param chart  the chart.
485     * @param useBuffer  a flag controlling whether or not an off-screen buffer
486     *                   is used (read the warning above before setting this
487     *                   to {@code false}).
488     */
489    public ChartPanel(JFreeChart chart, boolean useBuffer) {
490
491        this(chart, DEFAULT_WIDTH, DEFAULT_HEIGHT, DEFAULT_MINIMUM_DRAW_WIDTH,
492                DEFAULT_MINIMUM_DRAW_HEIGHT, DEFAULT_MAXIMUM_DRAW_WIDTH,
493                DEFAULT_MAXIMUM_DRAW_HEIGHT, useBuffer,
494                true,  // properties
495                true,  // save
496                true,  // print
497                true,  // zoom
498                true   // tooltips
499                );
500
501    }
502
503    /**
504     * Constructs a JFreeChart panel.
505     *
506     * @param chart  the chart.
507     * @param properties  a flag indicating whether or not the chart property
508     *                    editor should be available via the popup menu.
509     * @param save  a flag indicating whether or not save options should be
510     *              available via the popup menu.
511     * @param print  a flag indicating whether or not the print option
512     *               should be available via the popup menu.
513     * @param zoom  a flag indicating whether or not zoom options should
514     *              be added to the popup menu.
515     * @param tooltips  a flag indicating whether or not tooltips should be
516     *                  enabled for the chart.
517     */
518    public ChartPanel(JFreeChart chart, boolean properties, boolean save,
519            boolean print, boolean zoom, boolean tooltips) {
520
521        this(chart, DEFAULT_WIDTH, DEFAULT_HEIGHT,
522             DEFAULT_MINIMUM_DRAW_WIDTH, DEFAULT_MINIMUM_DRAW_HEIGHT,
523             DEFAULT_MAXIMUM_DRAW_WIDTH, DEFAULT_MAXIMUM_DRAW_HEIGHT,
524             DEFAULT_BUFFER_USED, properties, save, print, zoom, tooltips);
525
526    }
527
528    /**
529     * Constructs a JFreeChart panel.
530     *
531     * @param chart  the chart.
532     * @param width  the preferred width of the panel.
533     * @param height  the preferred height of the panel.
534     * @param minimumDrawWidth  the minimum drawing width.
535     * @param minimumDrawHeight  the minimum drawing height.
536     * @param maximumDrawWidth  the maximum drawing width.
537     * @param maximumDrawHeight  the maximum drawing height.
538     * @param useBuffer  a flag that indicates whether to use the off-screen
539     *                   buffer to improve performance (at the expense of
540     *                   memory).
541     * @param properties  a flag indicating whether or not the chart property
542     *                    editor should be available via the popup menu.
543     * @param save  a flag indicating whether or not save options should be
544     *              available via the popup menu.
545     * @param print  a flag indicating whether or not the print option
546     *               should be available via the popup menu.
547     * @param zoom  a flag indicating whether or not zoom options should be
548     *              added to the popup menu.
549     * @param tooltips  a flag indicating whether or not tooltips should be
550     *                  enabled for the chart.
551     */
552    public ChartPanel(JFreeChart chart, int width, int height,
553            int minimumDrawWidth, int minimumDrawHeight, int maximumDrawWidth,
554            int maximumDrawHeight, boolean useBuffer, boolean properties,
555            boolean save, boolean print, boolean zoom, boolean tooltips) {
556
557        this(chart, width, height, minimumDrawWidth, minimumDrawHeight,
558                maximumDrawWidth, maximumDrawHeight, useBuffer, properties,
559                true, save, print, zoom, tooltips);
560    }
561
562    /**
563     * Constructs a JFreeChart panel.
564     *
565     * @param chart  the chart.
566     * @param width  the preferred width of the panel.
567     * @param height  the preferred height of the panel.
568     * @param minimumDrawWidth  the minimum drawing width.
569     * @param minimumDrawHeight  the minimum drawing height.
570     * @param maximumDrawWidth  the maximum drawing width.
571     * @param maximumDrawHeight  the maximum drawing height.
572     * @param useBuffer  a flag that indicates whether to use the off-screen
573     *                   buffer to improve performance (at the expense of
574     *                   memory).
575     * @param properties  a flag indicating whether or not the chart property
576     *                    editor should be available via the popup menu.
577     * @param copy  a flag indicating whether or not a copy option should be
578     *              available via the popup menu.
579     * @param save  a flag indicating whether or not save options should be
580     *              available via the popup menu.
581     * @param print  a flag indicating whether or not the print option
582     *               should be available via the popup menu.
583     * @param zoom  a flag indicating whether or not zoom options should be
584     *              added to the popup menu.
585     * @param tooltips  a flag indicating whether or not tooltips should be
586     *                  enabled for the chart.
587     */
588    public ChartPanel(JFreeChart chart, int width, int height,
589           int minimumDrawWidth, int minimumDrawHeight, int maximumDrawWidth,
590           int maximumDrawHeight, boolean useBuffer, boolean properties,
591           boolean copy, boolean save, boolean print, boolean zoom,
592           boolean tooltips) {
593
594        setChart(chart);
595        this.chartMouseListeners = new EventListenerList();
596        this.info = new ChartRenderingInfo();
597        setPreferredSize(new Dimension(width, height));
598        this.useBuffer = useBuffer;
599        this.refreshBuffer = false;
600        this.minimumDrawWidth = minimumDrawWidth;
601        this.minimumDrawHeight = minimumDrawHeight;
602        this.maximumDrawWidth = maximumDrawWidth;
603        this.maximumDrawHeight = maximumDrawHeight;
604
605        // set up popup menu...
606        this.popup = null;
607        if (properties || copy || save || print || zoom) {
608            this.popup = createPopupMenu(properties, copy, save, print, zoom);
609        }
610
611        enableEvents(AWTEvent.MOUSE_EVENT_MASK);
612        enableEvents(AWTEvent.MOUSE_MOTION_EVENT_MASK);
613        setDisplayToolTips(tooltips);
614        addMouseListener(this);
615        addMouseMotionListener(this);
616
617        this.defaultDirectoryForSaveAs = null;
618        this.enforceFileExtensions = true;
619
620        // initialize ChartPanel-specific tool tip delays with
621        // values the from ToolTipManager.sharedInstance()
622        ToolTipManager ttm = ToolTipManager.sharedInstance();
623        this.ownToolTipInitialDelay = ttm.getInitialDelay();
624        this.ownToolTipDismissDelay = ttm.getDismissDelay();
625        this.ownToolTipReshowDelay = ttm.getReshowDelay();
626
627        this.zoomAroundAnchor = false;
628
629        this.overlays = new ArrayList<>();
630    }
631
632    /**
633     * Returns the chart contained in the panel.
634     *
635     * @return The chart (possibly {@code null}).
636     */
637    public JFreeChart getChart() {
638        return this.chart;
639    }
640
641    /**
642     * Sets the chart that is displayed in the panel.
643     *
644     * @param chart  the chart ({@code null} permitted).
645     */
646    public void setChart(JFreeChart chart) {
647
648        // stop listening for changes to the existing chart
649        if (this.chart != null) {
650            this.chart.removeChangeListener(this);
651            this.chart.removeProgressListener(this);
652        }
653
654        // add the new chart
655        this.chart = chart;
656        if (chart != null) {
657            this.chart.addChangeListener(this);
658            this.chart.addProgressListener(this);
659            Plot plot = chart.getPlot();
660            this.domainZoomable = false;
661            this.rangeZoomable = false;
662            if (plot instanceof Zoomable) {
663                Zoomable z = (Zoomable) plot;
664                this.domainZoomable = z.isDomainZoomable();
665                this.rangeZoomable = z.isRangeZoomable();
666                this.orientation = z.getOrientation();
667            }
668        }
669        else {
670            this.domainZoomable = false;
671            this.rangeZoomable = false;
672        }
673        if (this.useBuffer) {
674            this.refreshBuffer = true;
675        }
676        repaint();
677
678    }
679
680    /**
681     * Returns the minimum drawing width for charts.
682     * <P>
683     * If the width available on the panel is less than this, then the chart is
684     * drawn at the minimum width then scaled down to fit.
685     *
686     * @return The minimum drawing width.
687     */
688    public int getMinimumDrawWidth() {
689        return this.minimumDrawWidth;
690    }
691
692    /**
693     * Sets the minimum drawing width for the chart on this panel.
694     * <P>
695     * At the time the chart is drawn on the panel, if the available width is
696     * less than this amount, the chart will be drawn using the minimum width
697     * then scaled down to fit the available space.
698     *
699     * @param width  The width.
700     */
701    public void setMinimumDrawWidth(int width) {
702        this.minimumDrawWidth = width;
703    }
704
705    /**
706     * Returns the maximum drawing width for charts.
707     * <P>
708     * If the width available on the panel is greater than this, then the chart
709     * is drawn at the maximum width then scaled up to fit.
710     *
711     * @return The maximum drawing width.
712     */
713    public int getMaximumDrawWidth() {
714        return this.maximumDrawWidth;
715    }
716
717    /**
718     * Sets the maximum drawing width for the chart on this panel.
719     * <P>
720     * At the time the chart is drawn on the panel, if the available width is
721     * greater than this amount, the chart will be drawn using the maximum
722     * width then scaled up to fit the available space.
723     *
724     * @param width  The width.
725     */
726    public void setMaximumDrawWidth(int width) {
727        this.maximumDrawWidth = width;
728    }
729
730    /**
731     * Returns the minimum drawing height for charts.
732     * <P>
733     * If the height available on the panel is less than this, then the chart
734     * is drawn at the minimum height then scaled down to fit.
735     *
736     * @return The minimum drawing height.
737     */
738    public int getMinimumDrawHeight() {
739        return this.minimumDrawHeight;
740    }
741
742    /**
743     * Sets the minimum drawing height for the chart on this panel.
744     * <P>
745     * At the time the chart is drawn on the panel, if the available height is
746     * less than this amount, the chart will be drawn using the minimum height
747     * then scaled down to fit the available space.
748     *
749     * @param height  The height.
750     */
751    public void setMinimumDrawHeight(int height) {
752        this.minimumDrawHeight = height;
753    }
754
755    /**
756     * Returns the maximum drawing height for charts.
757     * <P>
758     * If the height available on the panel is greater than this, then the
759     * chart is drawn at the maximum height then scaled up to fit.
760     *
761     * @return The maximum drawing height.
762     */
763    public int getMaximumDrawHeight() {
764        return this.maximumDrawHeight;
765    }
766
767    /**
768     * Sets the maximum drawing height for the chart on this panel.
769     * <P>
770     * At the time the chart is drawn on the panel, if the available height is
771     * greater than this amount, the chart will be drawn using the maximum
772     * height then scaled up to fit the available space.
773     *
774     * @param height  The height.
775     */
776    public void setMaximumDrawHeight(int height) {
777        this.maximumDrawHeight = height;
778    }
779
780    /**
781     * Returns the X scale factor for the chart.  This will be 1.0 if no
782     * scaling has been used.
783     *
784     * @return The scale factor.
785     */
786    public double getScaleX() {
787        return this.scaleX;
788    }
789
790    /**
791     * Returns the Y scale factory for the chart.  This will be 1.0 if no
792     * scaling has been used.
793     *
794     * @return The scale factor.
795     */
796    public double getScaleY() {
797        return this.scaleY;
798    }
799
800    /**
801     * Returns the anchor point.
802     *
803     * @return The anchor point (possibly {@code null}).
804     */
805    public Point2D getAnchor() {
806        return this.anchor;
807    }
808
809    /**
810     * Sets the anchor point.  This method is provided for the use of
811     * subclasses, not end users.
812     *
813     * @param anchor  the anchor point ({@code null} permitted).
814     */
815    protected void setAnchor(Point2D anchor) {
816        this.anchor = anchor;
817    }
818
819    /**
820     * Returns the popup menu.
821     *
822     * @return The popup menu.
823     */
824    public JPopupMenu getPopupMenu() {
825        return this.popup;
826    }
827
828    /**
829     * Sets the popup menu for the panel.
830     *
831     * @param popup  the popup menu ({@code null} permitted).
832     */
833    public void setPopupMenu(JPopupMenu popup) {
834        this.popup = popup;
835    }
836
837    /**
838     * Returns the chart rendering info from the most recent chart redraw.
839     *
840     * @return The chart rendering info.
841     */
842    public ChartRenderingInfo getChartRenderingInfo() {
843        return this.info;
844    }
845
846    /**
847     * A convenience method that switches on mouse-based zooming.
848     *
849     * @param flag  {@code true} enables zooming and rectangle fill on
850     *              zoom.
851     */
852    public void setMouseZoomable(boolean flag) {
853        setMouseZoomable(flag, true);
854    }
855
856    /**
857     * A convenience method that switches on mouse-based zooming.
858     *
859     * @param flag  {@code true} if zooming enabled
860     * @param fillRectangle  {@code true} if zoom rectangle is filled,
861     *                       false if rectangle is shown as outline only.
862     */
863    public void setMouseZoomable(boolean flag, boolean fillRectangle) {
864        setDomainZoomable(flag);
865        setRangeZoomable(flag);
866        setFillZoomRectangle(fillRectangle);
867    }
868
869    /**
870     * Sets default modifier keys for pan operations for all mouse buttons.
871     * Modifiers for a specific button can be set with
872     * {@link #setPanModifiersEx(int, int)}.  If there are none set for
873     * a certain button, it will use the modifiers passed to this function,
874     * defaulting to {@link #getDefaultDragModifiersEx()} if this function
875     * was never called.
876     * <p>
877     * Only {@link InputEvent#SHIFT_DOWN_MASK}, {@link InputEvent#CTRL_DOWN_MASK},
878     * {@link InputEvent#META_DOWN_MASK} and {@link InputEvent#ALT_DOWN_MASK} are
879     * checked.  To avoid platform-specific problems, it is recommended to use
880     * {@link #getDefaultDragModifiersEx()} for one operation, and zero modifiers
881     * for the other.
882     * <p>
883     * If the same modifiers are set for both zooming and panning,
884     * panning will be performed.
885     *
886     * @param modifiersEx modifier keys, as returned by {@link InputEvent#getModifiersEx()}
887     */
888    public void setDefaultPanModifiersEx(int modifiersEx) {
889        this.panMask = modifiersEx;
890    }
891
892    /**
893     * Sets default modifier keys for zoom operations for all mouse buttons.
894     * Modifiers for a specific button can be set with
895     * {@link #setZoomModifiersEx(int, int)}.  If there are none set for
896     * a certain button, it will use the modifiers passed to this function,
897     * defaulting to zero (no modifiers) if this function was never called.
898     * <p>
899     * Only {@link InputEvent#SHIFT_DOWN_MASK}, {@link InputEvent#CTRL_DOWN_MASK},
900     * {@link InputEvent#META_DOWN_MASK} and {@link InputEvent#ALT_DOWN_MASK} are
901     * checked.  To avoid platform-specific problems, it is recommended to use
902     * {@link #getDefaultDragModifiersEx()} for one operation, and zero modifiers
903     * for the other.
904     * <p>
905     * If the same modifiers are set for both zooming and panning,
906     * panning will be performed.
907     *
908     * @param modifiersEx modifier keys, as returned by {@link InputEvent#getModifiersEx()}
909     */
910    public void setDefaultZoomModifiersEx(int modifiersEx) {
911        this.zoomMask = modifiersEx;
912    }
913
914    /**
915     * Sets modifier keys for panning with a specific mouse button. If there are
916     * none set for a certain button with this function, default modifiers set
917     * with {@link #setDefaultPanModifiersEx(int)} will be used, defaulting to
918     * {@link #getDefaultDragModifiersEx()} if none were set either.<p>
919     * Only {@link InputEvent#SHIFT_DOWN_MASK}, {@link InputEvent#CTRL_DOWN_MASK},
920     * {@link InputEvent#META_DOWN_MASK} and {@link InputEvent#ALT_DOWN_MASK} are
921     * checked.  To avoid platform-specific problems, it is recommended to use
922     * {@link #getDefaultDragModifiersEx()} for one operation, and zero modifiers
923     * for the other.
924     * <p>
925     * If the same modifiers are set for both zooming and panning,
926     * panning will be performed.
927     *
928     * @param mouseButton  the mouse button
929     * @param modifiersEx modifier keys, as returned by {@link InputEvent#getModifiersEx()}
930     */
931    public void setPanModifiersEx(int mouseButton, int modifiersEx) {
932        panButtonMasks.put(mouseButton, modifiersEx);
933    }
934
935    /**
936     * Sets modifier keys for zooming with a specific mouse button.
937     * If there are none set for a certain button with this function,
938     * default modifiers set with {@link #setDefaultZoomModifiersEx(int)}
939     * will be used, defaulting to zero (no modifiers)
940     * if none were set either.
941     * <p>
942     * Only {@link InputEvent#SHIFT_DOWN_MASK}, {@link InputEvent#CTRL_DOWN_MASK},
943     * {@link InputEvent#META_DOWN_MASK} and {@link InputEvent#ALT_DOWN_MASK} are
944     * checked.  To avoid platform-specific problems, it is recommended to use
945     * {@link #getDefaultDragModifiersEx()} for one operation, and zero modifiers
946     * for the other.
947     * <p>
948     * If the same modifiers are set for both zooming and panning,
949     * panning will be performed.
950     *
951     * @param mouseButton  the mouse button.
952     * @param modifiersEx modifier keys, as returned by {@link InputEvent#getModifiersEx()}
953     */
954    public void setZoomModifiersEx(int mouseButton, int modifiersEx) {
955        zoomButtonMasks.put(mouseButton, modifiersEx);
956    }
957
958    /**
959     * Returns the flag that determines whether or not zooming is enabled for
960     * the domain axis.
961     *
962     * @return A boolean.
963     */
964    public boolean isDomainZoomable() {
965        return this.domainZoomable;
966    }
967
968    /**
969     * Sets the flag that controls whether or not zooming is enabled for the
970     * domain axis.  A check is made to ensure that the current plot supports
971     * zooming for the domain values.
972     *
973     * @param flag  {@code true} enables zooming if possible.
974     */
975    public void setDomainZoomable(boolean flag) {
976        if (flag) {
977            Plot plot = this.chart.getPlot();
978            if (plot instanceof Zoomable) {
979                Zoomable z = (Zoomable) plot;
980                this.domainZoomable = z.isDomainZoomable();
981            }
982        }
983        else {
984            this.domainZoomable = false;
985        }
986    }
987
988    /**
989     * Returns the flag that determines whether or not zooming is enabled for
990     * the range axis.
991     *
992     * @return A boolean.
993     */
994    public boolean isRangeZoomable() {
995        return this.rangeZoomable;
996    }
997
998    /**
999     * A flag that controls mouse-based zooming on the vertical axis.
1000     *
1001     * @param flag  {@code true} enables zooming.
1002     */
1003    public void setRangeZoomable(boolean flag) {
1004        if (flag) {
1005            Plot plot = this.chart.getPlot();
1006            if (plot instanceof Zoomable) {
1007                Zoomable z = (Zoomable) plot;
1008                this.rangeZoomable = z.isRangeZoomable();
1009            }
1010        }
1011        else {
1012            this.rangeZoomable = false;
1013        }
1014    }
1015
1016    /**
1017     * Returns a strategy used to control and draw zoom rectangle.
1018     *
1019     * @return A zoom rectangle strategy.
1020     */
1021    public SelectionZoomStrategy getSelectionZoomStrategy() {
1022        return selectionZoomStrategy;
1023    }
1024
1025    /**
1026     * A strategy used to control and draw zoom rectangle.
1027     *
1028     * @param selectionZoomStrategy  A zoom rectangle strategy.
1029     */
1030    public void setSelectionZoomStrategy(SelectionZoomStrategy selectionZoomStrategy) {
1031        this.selectionZoomStrategy = selectionZoomStrategy;
1032    }
1033
1034    /**
1035     * Returns the flag that controls whether or not the zoom rectangle is
1036     * filled when drawn.
1037     *
1038     * @return A boolean.
1039     */
1040    public boolean getFillZoomRectangle() {
1041        return this.selectionZoomStrategy.getFillZoomRectangle();
1042    }
1043
1044    /**
1045     * A flag that controls how the zoom rectangle is drawn.
1046     *
1047     * @param flag  {@code true} instructs to fill the rectangle on
1048     *              zoom, otherwise it will be outlined.
1049     */
1050    public void setFillZoomRectangle(boolean flag) {
1051        this.selectionZoomStrategy.setFillZoomRectangle(flag);
1052    }
1053
1054    /**
1055     * Returns the zoom trigger distance.  This controls how far the mouse must
1056     * move before a zoom action is triggered.
1057     *
1058     * @return The distance (in Java2D units).
1059     */
1060    public int getZoomTriggerDistance() {
1061        return this.selectionZoomStrategy.getZoomTriggerDistance();
1062    }
1063
1064    /**
1065     * Sets the zoom trigger distance.  This controls how far the mouse must
1066     * move before a zoom action is triggered.
1067     *
1068     * @param distance  the distance (in Java2D units).
1069     */
1070    public void setZoomTriggerDistance(int distance) {
1071        this.selectionZoomStrategy.setZoomTriggerDistance(distance);
1072    }
1073
1074    /**
1075     * Returns the default directory for the "save as" option.
1076     *
1077     * @return The default directory (possibly {@code null}).
1078     */
1079    public File getDefaultDirectoryForSaveAs() {
1080        return this.defaultDirectoryForSaveAs;
1081    }
1082
1083    /**
1084     * Sets the default directory for the "save as" option.  If you set this
1085     * to {@code null}, the user's default directory will be used.
1086     *
1087     * @param directory  the directory ({@code null} permitted).
1088     */
1089    public void setDefaultDirectoryForSaveAs(File directory) {
1090        if (directory != null) {
1091            if (!directory.isDirectory()) {
1092                throw new IllegalArgumentException(
1093                        "The 'directory' argument is not a directory.");
1094            }
1095        }
1096        this.defaultDirectoryForSaveAs = directory;
1097    }
1098
1099    /**
1100     * Returns {@code true} if file extensions should be enforced, and
1101     * {@code false} otherwise.
1102     *
1103     * @return The flag.
1104     *
1105     * @see #setEnforceFileExtensions(boolean)
1106     */
1107    public boolean isEnforceFileExtensions() {
1108        return this.enforceFileExtensions;
1109    }
1110
1111    /**
1112     * Sets a flag that controls whether or not file extensions are enforced.
1113     *
1114     * @param enforce  the new flag value.
1115     *
1116     * @see #isEnforceFileExtensions()
1117     */
1118    public void setEnforceFileExtensions(boolean enforce) {
1119        this.enforceFileExtensions = enforce;
1120    }
1121
1122    /**
1123     * Returns the flag that controls whether or not zoom operations are
1124     * centered around the current anchor point.
1125     *
1126     * @return A boolean.
1127     *
1128     * @see #setZoomAroundAnchor(boolean)
1129     */
1130    public boolean getZoomAroundAnchor() {
1131        return this.zoomAroundAnchor;
1132    }
1133
1134    /**
1135     * Sets the flag that controls whether or not zoom operations are
1136     * centered around the current anchor point.
1137     *
1138     * @param zoomAroundAnchor  the new flag value.
1139     *
1140     * @see #getZoomAroundAnchor()
1141     */
1142    public void setZoomAroundAnchor(boolean zoomAroundAnchor) {
1143        this.zoomAroundAnchor = zoomAroundAnchor;
1144    }
1145
1146    /**
1147     * Returns the zoom rectangle fill paint.
1148     *
1149     * @return The zoom rectangle fill paint (never {@code null}).
1150     *
1151     * @see #setZoomFillPaint(java.awt.Paint)
1152     * @see #setFillZoomRectangle(boolean)
1153     */
1154    public Paint getZoomFillPaint() {
1155        return selectionZoomStrategy.getZoomFillPaint();
1156    }
1157
1158    /**
1159     * Sets the zoom rectangle fill paint.
1160     *
1161     * @param paint  the paint ({@code null} not permitted).
1162     *
1163     * @see #getZoomFillPaint()
1164     * @see #getFillZoomRectangle()
1165     */
1166    public void setZoomFillPaint(Paint paint) {
1167        selectionZoomStrategy.setZoomFillPaint(paint);
1168    }
1169
1170    /**
1171     * Returns the zoom rectangle outline paint.
1172     *
1173     * @return The zoom rectangle outline paint (never {@code null}).
1174     *
1175     * @see #setZoomOutlinePaint(java.awt.Paint)
1176     * @see #setFillZoomRectangle(boolean)
1177     */
1178    public Paint getZoomOutlinePaint() {
1179        return selectionZoomStrategy.getZoomOutlinePaint();
1180    }
1181
1182    /**
1183     * Sets the zoom rectangle outline paint.
1184     *
1185     * @param paint  the paint ({@code null} not permitted).
1186     *
1187     * @see #getZoomOutlinePaint()
1188     * @see #getFillZoomRectangle()
1189     */
1190    public void setZoomOutlinePaint(Paint paint) {
1191        this.selectionZoomStrategy.setZoomOutlinePaint(paint);
1192    }
1193
1194    /**
1195     * The mouse wheel handler.
1196     */
1197    protected MouseWheelHandler mouseWheelHandler;
1198
1199    /**
1200     * Returns {@code true} if the mouse wheel handler is enabled, and
1201     * {@code false} otherwise.
1202     *
1203     * @return A boolean.
1204     */
1205    public boolean isMouseWheelEnabled() {
1206        return this.mouseWheelHandler != null;
1207    }
1208
1209    /**
1210     * Enables or disables mouse wheel support for the panel.
1211     *
1212     * @param flag  a boolean.
1213     */
1214    public void setMouseWheelEnabled(boolean flag) {
1215        if (flag && this.mouseWheelHandler == null) {
1216            this.mouseWheelHandler = new MouseWheelHandler(this);
1217        }
1218        else if (!flag && this.mouseWheelHandler != null) {
1219            this.removeMouseWheelListener(this.mouseWheelHandler);
1220            this.mouseWheelHandler = null;
1221        } 
1222    }
1223
1224    /**
1225     * Add an overlay to the panel.
1226     *
1227     * @param overlay  the overlay ({@code null} not permitted).
1228     */
1229    public void addOverlay(Overlay overlay) {
1230        Args.nullNotPermitted(overlay, "overlay");
1231        this.overlays.add(overlay);
1232        overlay.addChangeListener(this);
1233        repaint();
1234    }
1235
1236    /**
1237     * Removes an overlay from the panel.
1238     *
1239     * @param overlay  the overlay to remove ({@code null} not permitted).
1240     */
1241    public void removeOverlay(Overlay overlay) {
1242        Args.nullNotPermitted(overlay, "overlay");
1243        boolean removed = this.overlays.remove(overlay);
1244        if (removed) {
1245            overlay.removeChangeListener(this);
1246            repaint();
1247        }
1248    }
1249
1250    /**
1251     * Handles a change to an overlay by repainting the panel.
1252     *
1253     * @param event  the event.
1254     */
1255    @Override
1256    public void overlayChanged(OverlayChangeEvent event) {
1257        repaint();
1258    }
1259
1260    /**
1261     * Switches the display of tooltips for the panel on or off.  Note that
1262     * tooltips can only be displayed if the chart has been configured to
1263     * generate tooltip items.
1264     *
1265     * @param flag  {@code true} to enable tooltips, {@code false} to
1266     *              disable tooltips.
1267     */
1268    public void setDisplayToolTips(boolean flag) {
1269        if (flag) {
1270            ToolTipManager.sharedInstance().registerComponent(this);
1271        }
1272        else {
1273            ToolTipManager.sharedInstance().unregisterComponent(this);
1274        }
1275    }
1276
1277    /**
1278     * Returns a string for the tooltip.
1279     *
1280     * @param e  the mouse event.
1281     *
1282     * @return A tool tip or {@code null} if no tooltip is available.
1283     */
1284    @Override
1285    public String getToolTipText(MouseEvent e) {
1286        String result = null;
1287        if (this.info != null) {
1288            EntityCollection entities = this.info.getEntityCollection();
1289            if (entities != null) {
1290                Insets insets = getInsets();
1291                ChartEntity entity = entities.getEntity(
1292                        (int) ((e.getX() - insets.left) / this.scaleX),
1293                        (int) ((e.getY() - insets.top) / this.scaleY));
1294                if (entity != null) {
1295                    result = entity.getToolTipText();
1296                }
1297            }
1298        }
1299        return result;
1300    }
1301
1302    /**
1303     * Translates a Java2D point on the chart to a screen location.
1304     *
1305     * @param java2DPoint  the Java2D point.
1306     *
1307     * @return The screen location.
1308     */
1309    public Point translateJava2DToScreen(Point2D java2DPoint) {
1310        Insets insets = getInsets();
1311        int x = (int) (java2DPoint.getX() * this.scaleX + insets.left);
1312        int y = (int) (java2DPoint.getY() * this.scaleY + insets.top);
1313        return new Point(x, y);
1314    }
1315
1316    /**
1317     * Translates a panel (component) location to a Java2D point.
1318     *
1319     * @param screenPoint  the screen location ({@code null} not
1320     *                     permitted).
1321     *
1322     * @return The Java2D coordinates.
1323     */
1324    public Point2D translateScreenToJava2D(Point screenPoint) {
1325        Insets insets = getInsets();
1326        double x = (screenPoint.getX() - insets.left) / this.scaleX;
1327        double y = (screenPoint.getY() - insets.top) / this.scaleY;
1328        return new Point2D.Double(x, y);
1329    }
1330
1331    /**
1332     * Applies any scaling that is in effect for the chart drawing to the
1333     * given rectangle.
1334     *
1335     * @param rect  the rectangle ({@code null} not permitted).
1336     *
1337     * @return A new scaled rectangle.
1338     */
1339    public Rectangle2D scale(Rectangle2D rect) {
1340        Insets insets = getInsets();
1341        double x = rect.getX() * getScaleX() + insets.left;
1342        double y = rect.getY() * getScaleY() + insets.top;
1343        double w = rect.getWidth() * getScaleX();
1344        double h = rect.getHeight() * getScaleY();
1345        return new Rectangle2D.Double(x, y, w, h);
1346    }
1347
1348    /**
1349     * Returns the chart entity at a given point.
1350     * <P>
1351     * This method will return null if there is (a) no entity at the given
1352     * point, or (b) no entity collection has been generated.
1353     *
1354     * @param viewX  the x-coordinate.
1355     * @param viewY  the y-coordinate.
1356     *
1357     * @return The chart entity (possibly {@code null}).
1358     */
1359    public ChartEntity getEntityForPoint(int viewX, int viewY) {
1360
1361        ChartEntity result = null;
1362        if (this.info != null) {
1363            Insets insets = getInsets();
1364            double x = (viewX - insets.left) / this.scaleX;
1365            double y = (viewY - insets.top) / this.scaleY;
1366            EntityCollection entities = this.info.getEntityCollection();
1367            result = entities != null ? entities.getEntity(x, y) : null;
1368        }
1369        return result;
1370
1371    }
1372
1373    /**
1374     * Returns the flag that controls whether or not the offscreen buffer
1375     * needs to be refreshed.
1376     *
1377     * @return A boolean.
1378     */
1379    public boolean getRefreshBuffer() {
1380        return this.refreshBuffer;
1381    }
1382
1383    /**
1384     * Sets the refresh buffer flag.  This flag is used to avoid unnecessary
1385     * redrawing of the chart when the offscreen image buffer is used.
1386     *
1387     * @param flag  {@code true} indicates that the buffer should be
1388     *              refreshed.
1389     */
1390    public void setRefreshBuffer(boolean flag) {
1391        this.refreshBuffer = flag;
1392    }
1393
1394    /**
1395     * Paints the component by drawing the chart to fill the entire component,
1396     * but allowing for the insets (which will be non-zero if a border has been
1397     * set for this component).  To increase performance (at the expense of
1398     * memory), an off-screen buffer image can be used.
1399     *
1400     * @param g  the graphics device for drawing on.
1401     */
1402    @Override
1403    public void paintComponent(Graphics g) {
1404        super.paintComponent(g);
1405        if (this.chart == null) {
1406            return;
1407        }
1408        Graphics2D g2 = (Graphics2D) g.create();
1409        
1410        // first determine the size of the chart rendering area...
1411        Dimension size = getSize();
1412        Insets insets = getInsets();
1413        Rectangle2D available = new Rectangle2D.Double(insets.left, insets.top,
1414                size.getWidth() - insets.left - insets.right,
1415                size.getHeight() - insets.top - insets.bottom);
1416
1417        // work out if scaling is required...
1418        boolean scale = false;
1419        double drawWidth = available.getWidth();
1420        double drawHeight = available.getHeight();
1421        this.scaleX = 1.0;
1422        this.scaleY = 1.0;
1423
1424        if (drawWidth < this.minimumDrawWidth) {
1425            this.scaleX = drawWidth / this.minimumDrawWidth;
1426            drawWidth = this.minimumDrawWidth;
1427            scale = true;
1428        }
1429        else if (drawWidth > this.maximumDrawWidth) {
1430            this.scaleX = drawWidth / this.maximumDrawWidth;
1431            drawWidth = this.maximumDrawWidth;
1432            scale = true;
1433        }
1434
1435        if (drawHeight < this.minimumDrawHeight) {
1436            this.scaleY = drawHeight / this.minimumDrawHeight;
1437            drawHeight = this.minimumDrawHeight;
1438            scale = true;
1439        }
1440        else if (drawHeight > this.maximumDrawHeight) {
1441            this.scaleY = drawHeight / this.maximumDrawHeight;
1442            drawHeight = this.maximumDrawHeight;
1443            scale = true;
1444        }
1445
1446        Rectangle2D chartArea = new Rectangle2D.Double(0.0, 0.0, drawWidth,
1447                drawHeight);
1448
1449        // are we using the chart buffer?
1450        if (this.useBuffer) {
1451
1452            // for better rendering on the HiDPI monitors upscaling the buffer to the "native" resoution
1453            // instead of using logical one provided by Swing
1454            final AffineTransform globalTransform = ((Graphics2D) g).getTransform();
1455            final double globalScaleX = globalTransform.getScaleX();
1456            final double globalScaleY = globalTransform.getScaleY();
1457
1458            final int scaledWidth = (int) (available.getWidth() * globalScaleX);
1459            final int scaledHeight = (int) (available.getHeight() * globalScaleY);
1460
1461            // do we need to resize the buffer?
1462            if ((this.chartBuffer == null)
1463                    || (this.chartBufferWidth != scaledWidth)
1464                    || (this.chartBufferHeight != scaledHeight)) {
1465                this.chartBufferWidth = scaledWidth;
1466                this.chartBufferHeight = scaledHeight;
1467                GraphicsConfiguration gc = g2.getDeviceConfiguration();
1468                this.chartBuffer = gc.createCompatibleImage(
1469                        this.chartBufferWidth, this.chartBufferHeight,
1470                        Transparency.TRANSLUCENT);
1471                this.refreshBuffer = true;
1472            }
1473
1474            // do we need to redraw the buffer?
1475            if (this.refreshBuffer) {
1476
1477                this.refreshBuffer = false; // clear the flag
1478
1479                // scale graphics of the buffer to the same value as global
1480                // Swing graphics - this allow to paint all elements as usual
1481                // but applies all necessary smoothing
1482                Graphics2D bufferG2 = (Graphics2D) this.chartBuffer.getGraphics();
1483                bufferG2.scale(globalScaleX, globalScaleY);
1484
1485                Rectangle2D bufferArea = new Rectangle2D.Double(
1486                        0, 0, available.getWidth(), available.getHeight());
1487
1488                // make the background of the buffer clear and transparent
1489                Composite savedComposite = bufferG2.getComposite();
1490                bufferG2.setComposite(AlphaComposite.getInstance(AlphaComposite.CLEAR, 0.0f));
1491                Rectangle r = new Rectangle(0, 0, (int) available.getWidth(), (int) available.getHeight());
1492                bufferG2.fill(r);
1493                bufferG2.setComposite(savedComposite);
1494                
1495                if (scale) {
1496                    AffineTransform saved = bufferG2.getTransform();
1497                    AffineTransform st = AffineTransform.getScaleInstance(
1498                            this.scaleX, this.scaleY);
1499                    bufferG2.transform(st);
1500                    this.chart.draw(bufferG2, chartArea, this.anchor,
1501                            this.info);
1502                    bufferG2.setTransform(saved);
1503                } else {
1504                    this.chart.draw(bufferG2, bufferArea, this.anchor,
1505                            this.info);
1506                }
1507                bufferG2.dispose();
1508            }
1509
1510            // zap the buffer onto the panel...
1511            g2.drawImage(this.chartBuffer, insets.left, insets.top, (int) available.getWidth(), (int) available.getHeight(), this);
1512            g2.addRenderingHints(this.chart.getRenderingHints()); // bug#187
1513
1514        } else { // redrawing the chart every time...
1515            AffineTransform saved = g2.getTransform();
1516            g2.translate(insets.left, insets.top);
1517            if (scale) {
1518                AffineTransform st = AffineTransform.getScaleInstance(
1519                        this.scaleX, this.scaleY);
1520                g2.transform(st);
1521            }
1522            this.chart.draw(g2, chartArea, this.anchor, this.info);
1523            g2.setTransform(saved);
1524
1525        }
1526
1527        for (Overlay overlay : this.overlays) {
1528            overlay.paintOverlay(g2, this);
1529        }
1530
1531        // redraw the zoom rectangle (if present) - if useBuffer is false,
1532        // we use XOR so we can XOR the rectangle away again without redrawing
1533        // the chart
1534        selectionZoomStrategy.drawZoomRectangle(g2, !this.useBuffer);
1535
1536        g2.dispose();
1537
1538        this.anchor = null;
1539    }
1540
1541    /**
1542     * Receives notification of changes to the chart, and redraws the chart.
1543     *
1544     * @param event  details of the chart change event.
1545     */
1546    @Override
1547    public void chartChanged(ChartChangeEvent event) {
1548        this.refreshBuffer = true;
1549        Plot plot = this.chart.getPlot();
1550        if (plot instanceof Zoomable) {
1551            Zoomable z = (Zoomable) plot;
1552            this.orientation = z.getOrientation();
1553        }
1554        repaint();
1555    }
1556
1557    /**
1558     * Receives notification of a chart progress event.
1559     *
1560     * @param event  the event.
1561     */
1562    @Override
1563    public void chartProgress(ChartProgressEvent event) {
1564        // does nothing - override if necessary
1565    }
1566
1567    /**
1568     * Handles action events generated by the popup menu.
1569     *
1570     * @param event  the event.
1571     */
1572    @Override
1573    public void actionPerformed(ActionEvent event) {
1574
1575        String command = event.getActionCommand();
1576
1577        // many of the zoom methods need a screen location - all we have is
1578        // the zoomPoint, but it might be null.  Here we grab the x and y
1579        // coordinates, or use defaults...
1580        double screenX = -1.0;
1581        double screenY = -1.0;
1582        Point2D zoomPoint = this.selectionZoomStrategy.getZoomPoint();
1583        if (zoomPoint != null) {
1584            screenX = zoomPoint.getX();
1585            screenY = zoomPoint.getY();
1586        }
1587
1588        switch (command) {
1589        case PROPERTIES_COMMAND:
1590            doEditChartProperties();
1591            break;
1592        case COPY_COMMAND:
1593            doCopy();
1594            break;
1595        case SAVE_AS_PNG_COMMAND:
1596            try {
1597                doSaveAs();
1598            } catch (IOException e) {
1599                JOptionPane.showMessageDialog(this, "I/O error occurred.",
1600                        localizationResources.getString("Save_as_PNG"), JOptionPane.WARNING_MESSAGE);
1601            }
1602            break;
1603        case SAVE_AS_PNG_SIZE_COMMAND:
1604            try {
1605                final Dimension ss = Toolkit.getDefaultToolkit().getScreenSize();
1606                doSaveAs(ss.width, ss.height);
1607            } catch (IOException e) {
1608                JOptionPane.showMessageDialog(ChartPanel.this, "I/O error occurred.",
1609                        localizationResources.getString("Save_as_PNG"), JOptionPane.WARNING_MESSAGE);
1610            }
1611            break;
1612        case SAVE_AS_SVG_COMMAND:
1613            try {
1614                saveAsSVG(null);
1615            } catch (IOException e) {
1616                JOptionPane.showMessageDialog(this, "I/O error occurred.",
1617                        localizationResources.getString("Save_as_SVG"), JOptionPane.WARNING_MESSAGE);
1618            }
1619            break;
1620        case SAVE_AS_PDF_COMMAND:
1621            saveAsPDF(null);
1622            break;
1623        case PRINT_COMMAND:
1624            createChartPrintJob();
1625            break;
1626        case ZOOM_IN_BOTH_COMMAND:
1627            zoomInBoth(screenX, screenY);
1628            break;
1629        case ZOOM_IN_DOMAIN_COMMAND:
1630            zoomInDomain(screenX, screenY);
1631            break;
1632        case ZOOM_IN_RANGE_COMMAND:
1633            zoomInRange(screenX, screenY);
1634            break;
1635        case ZOOM_OUT_BOTH_COMMAND:
1636            zoomOutBoth(screenX, screenY);
1637            break;
1638        case ZOOM_OUT_DOMAIN_COMMAND:
1639            zoomOutDomain(screenX, screenY);
1640            break;
1641        case ZOOM_OUT_RANGE_COMMAND:
1642            zoomOutRange(screenX, screenY);
1643            break;
1644        case ZOOM_RESET_BOTH_COMMAND:
1645            restoreAutoBounds();
1646            break;
1647        case ZOOM_RESET_DOMAIN_COMMAND:
1648            restoreAutoDomainBounds();
1649            break;
1650        case ZOOM_RESET_RANGE_COMMAND:
1651            restoreAutoRangeBounds();
1652            break;
1653        }
1654    }
1655
1656    /**
1657     * Handles a 'mouse entered' event. This method changes the tooltip delays
1658     * of ToolTipManager.sharedInstance() to the possibly different values set
1659     * for this chart panel.
1660     *
1661     * @param e  the mouse event.
1662     */
1663    @Override
1664    public void mouseEntered(MouseEvent e) {
1665        if (!this.ownToolTipDelaysActive) {
1666            ToolTipManager ttm = ToolTipManager.sharedInstance();
1667
1668            this.originalToolTipInitialDelay = ttm.getInitialDelay();
1669            ttm.setInitialDelay(this.ownToolTipInitialDelay);
1670
1671            this.originalToolTipReshowDelay = ttm.getReshowDelay();
1672            ttm.setReshowDelay(this.ownToolTipReshowDelay);
1673
1674            this.originalToolTipDismissDelay = ttm.getDismissDelay();
1675            ttm.setDismissDelay(this.ownToolTipDismissDelay);
1676
1677            this.ownToolTipDelaysActive = true;
1678        }
1679    }
1680
1681    /**
1682     * Handles a 'mouse exited' event. This method resets the tooltip delays of
1683     * ToolTipManager.sharedInstance() to their
1684     * original values in effect before mouseEntered()
1685     *
1686     * @param e  the mouse event.
1687     */
1688    @Override
1689    public void mouseExited(MouseEvent e) {
1690        if (this.ownToolTipDelaysActive) {
1691            // restore original tooltip dealys
1692            ToolTipManager ttm = ToolTipManager.sharedInstance();
1693            ttm.setInitialDelay(this.originalToolTipInitialDelay);
1694            ttm.setReshowDelay(this.originalToolTipReshowDelay);
1695            ttm.setDismissDelay(this.originalToolTipDismissDelay);
1696            this.ownToolTipDelaysActive = false;
1697        }
1698    }
1699
1700    /**
1701     * Handles a 'mouse pressed' event.
1702     * <P>
1703     * This event is the popup trigger on Unix/Linux.  For Windows, the popup
1704     * trigger is the 'mouse released' event.
1705     *
1706     * @param e  The mouse event.
1707     */
1708    @Override
1709    public void mousePressed(MouseEvent e) {
1710        if (this.chart == null) {
1711            return;
1712        }
1713        Plot plot = this.chart.getPlot();
1714        int button = e.getButton();
1715        int mods = e.getModifiersEx();
1716        if ((mods & MODIFIERS_EX_MASK) == panButtonMasks.getOrDefault(button, panMask)) {
1717            // can we pan this plot?
1718            if (plot instanceof Pannable) {
1719                Pannable pannable = (Pannable) plot;
1720                if (pannable.isDomainPannable() || pannable.isRangePannable()) {
1721                    Rectangle2D screenDataArea = getScreenDataArea(e.getX(),
1722                            e.getY());
1723                    if (screenDataArea != null && screenDataArea.contains(
1724                            e.getPoint())) {
1725                        this.panW = screenDataArea.getWidth();
1726                        this.panH = screenDataArea.getHeight();
1727                        this.panLast = e.getPoint();
1728                        setCursor(Cursor.getPredefinedCursor(
1729                                Cursor.MOVE_CURSOR));
1730                    }
1731                }
1732                // the actual panning occurs later in the mouseDragged() 
1733                // method
1734            }
1735        }
1736        else if (!this.selectionZoomStrategy.isActivated()) {
1737            if ((mods & MODIFIERS_EX_MASK) == zoomButtonMasks.getOrDefault(button, zoomMask)) {
1738                Rectangle2D screenDataArea = getScreenDataArea(e.getX(), e.getY());
1739                if (screenDataArea != null) {
1740                Point2D zoomPoint = getPointInRectangle(e.getX(), e.getY(),
1741                            screenDataArea);
1742                selectionZoomStrategy.setZoomPoint(zoomPoint);
1743                }
1744                else {
1745                selectionZoomStrategy.setZoomPoint(null);
1746                }
1747            }
1748            if (e.isPopupTrigger()) {
1749                if (this.popup != null) {
1750                    displayPopupMenu(e.getX(), e.getY());
1751                }
1752            }
1753        }
1754    }
1755
1756    /**
1757     * Returns a point based on (x, y) but constrained to be within the bounds
1758     * of the given rectangle.  This method could be moved to JCommon.
1759     *
1760     * @param x  the x-coordinate.
1761     * @param y  the y-coordinate.
1762     * @param area  the rectangle ({@code null} not permitted).
1763     *
1764     * @return A point within the rectangle.
1765     */
1766    protected Point2D getPointInRectangle(int x, int y, Rectangle2D area) {
1767        double xx = Math.max(area.getMinX(), Math.min(x, area.getMaxX()));
1768        double yy = Math.max(area.getMinY(), Math.min(y, area.getMaxY()));
1769        return new Point2D.Double(xx, yy);
1770    }
1771
1772    /**
1773     * Handles a 'mouse dragged' event.
1774     *
1775     * @param e  the mouse event.
1776     */
1777    @Override
1778    public void mouseDragged(MouseEvent e) {
1779
1780        // if the popup menu has already been triggered, then ignore dragging...
1781        if (this.popup != null && this.popup.isShowing()) {
1782            return;
1783        }
1784
1785        // handle panning if we have a start point
1786        if (this.panLast != null) {
1787            double dx = e.getX() - this.panLast.getX();
1788            double dy = e.getY() - this.panLast.getY();
1789            if (dx == 0.0 && dy == 0.0) {
1790                return;
1791            }
1792            double wPercent = -dx / this.panW;
1793            double hPercent = dy / this.panH;
1794            boolean old = this.chart.getPlot().isNotify();
1795            this.chart.getPlot().setNotify(false);
1796            Pannable p = (Pannable) this.chart.getPlot();
1797            if (p.getOrientation() == PlotOrientation.VERTICAL) {
1798                p.panDomainAxes(wPercent, this.info.getPlotInfo(),
1799                        this.panLast);
1800                p.panRangeAxes(hPercent, this.info.getPlotInfo(),
1801                        this.panLast);
1802            }
1803            else {
1804                p.panDomainAxes(hPercent, this.info.getPlotInfo(),
1805                        this.panLast);
1806                p.panRangeAxes(wPercent, this.info.getPlotInfo(),
1807                        this.panLast);
1808            }
1809            this.panLast = e.getPoint();
1810            this.chart.getPlot().setNotify(old);
1811            return;
1812        }
1813
1814        // if no initial zoom point was set, ignore dragging...
1815        if (this.selectionZoomStrategy.getZoomPoint() == null) {
1816            return;
1817        }
1818        Graphics2D g2 = (Graphics2D) getGraphics();
1819
1820        // erase the previous zoom rectangle (if any).  We only need to do
1821        // this is we are using XOR mode, which we do when we're not using
1822        // the buffer (if there is a buffer, then at the end of this method we
1823        // just trigger a repaint)
1824        if (!this.useBuffer) {
1825            selectionZoomStrategy.drawZoomRectangle(g2, true);
1826        }
1827
1828        boolean hZoom, vZoom;
1829        if (this.orientation == PlotOrientation.HORIZONTAL) {
1830            hZoom = this.rangeZoomable;
1831            vZoom = this.domainZoomable;
1832        }
1833        else {
1834            hZoom = this.domainZoomable;
1835            vZoom = this.rangeZoomable;
1836        }
1837        Point2D zoomPoint = this.selectionZoomStrategy.getZoomPoint();
1838        Rectangle2D scaledDataArea = getScreenDataArea(
1839                (int) zoomPoint.getX(), (int) zoomPoint.getY());
1840
1841        selectionZoomStrategy.updateZoomRectangleSelection(e, hZoom, vZoom, scaledDataArea);
1842
1843        // Draw the new zoom rectangle...
1844        if (this.useBuffer) {
1845            repaint();
1846        }
1847        else {
1848            // with no buffer, we use XOR to draw the rectangle "over" the
1849            // chart...
1850            selectionZoomStrategy.drawZoomRectangle(g2, true);
1851        }
1852        g2.dispose();
1853
1854    }
1855
1856    /**
1857     * Handles a 'mouse released' event.  On Windows, we need to check if this
1858     * is a popup trigger, but only if we haven't already been tracking a zoom
1859     * rectangle.
1860     *
1861     * @param e  information about the event.
1862     */
1863    @Override
1864    public void mouseReleased(MouseEvent e) {
1865
1866        // if we've been panning, we need to reset now that the mouse is 
1867        // released...
1868        if (this.panLast != null) {
1869            this.panLast = null;
1870            setCursor(Cursor.getDefaultCursor());
1871        }
1872
1873        else if (this.selectionZoomStrategy.isActivated()) {
1874            boolean hZoom, vZoom;
1875            if (this.orientation == PlotOrientation.HORIZONTAL) {
1876                hZoom = this.rangeZoomable;
1877                vZoom = this.domainZoomable;
1878            }
1879            else {
1880                hZoom = this.domainZoomable;
1881                vZoom = this.rangeZoomable;
1882            }
1883
1884            Point2D zoomPoint = this.selectionZoomStrategy.getZoomPoint();
1885            boolean zoomTrigger1 = hZoom && Math.abs(e.getX()
1886                - zoomPoint.getX()) >= this.selectionZoomStrategy.getZoomTriggerDistance();
1887            boolean zoomTrigger2 = vZoom && Math.abs(e.getY()
1888                - zoomPoint.getY()) >= this.selectionZoomStrategy.getZoomTriggerDistance();
1889            if (zoomTrigger1 || zoomTrigger2) {
1890                if ((hZoom && (e.getX() < zoomPoint.getX()))
1891                    || (vZoom && (e.getY() < zoomPoint.getY()))) {
1892                    restoreAutoBounds();
1893                }
1894                else {
1895                    Rectangle2D screenDataArea = getScreenDataArea(
1896                            (int) zoomPoint.getX(),
1897                            (int) zoomPoint.getY());
1898
1899                    Rectangle2D zoomArea = selectionZoomStrategy.getZoomRectangle(hZoom, vZoom, screenDataArea);
1900                    zoom(zoomArea);
1901                }
1902                this.selectionZoomStrategy.reset();
1903            }
1904            else {
1905                // erase the zoom rectangle
1906                Graphics2D g2 = (Graphics2D) getGraphics();
1907                if (this.useBuffer) {
1908                    repaint();
1909                }
1910                else {
1911                    selectionZoomStrategy.drawZoomRectangle(g2, true);
1912                }
1913                g2.dispose();
1914                this.selectionZoomStrategy.reset();
1915            }
1916
1917        }
1918
1919        else if (e.isPopupTrigger()) {
1920            if (this.popup != null) {
1921                displayPopupMenu(e.getX(), e.getY());
1922            }
1923        }
1924
1925    }
1926
1927    /**
1928     * Receives notification of mouse clicks on the panel. These are
1929     * translated and passed on to any registered {@link ChartMouseListener}s.
1930     *
1931     * @param event  Information about the mouse event.
1932     */
1933    @Override
1934    public void mouseClicked(MouseEvent event) {
1935
1936        Insets insets = getInsets();
1937        int x = (int) ((event.getX() - insets.left) / this.scaleX);
1938        int y = (int) ((event.getY() - insets.top) / this.scaleY);
1939
1940        this.anchor = new Point2D.Double(x, y);
1941        if (this.chart == null) {
1942            return;
1943        }
1944        this.chart.setNotify(true);  // force a redraw
1945        // new entity code...
1946        Object[] listeners = this.chartMouseListeners.getListeners(
1947                ChartMouseListener.class);
1948        if (listeners.length == 0) {
1949            return;
1950        }
1951
1952        ChartEntity entity = null;
1953        if (this.info != null) {
1954            EntityCollection entities = this.info.getEntityCollection();
1955            if (entities != null) {
1956                entity = entities.getEntity(x, y);
1957            }
1958        }
1959        ChartMouseEvent chartEvent = new ChartMouseEvent(getChart(), event,
1960                entity);
1961        for (int i = listeners.length - 1; i >= 0; i -= 1) {
1962            ((ChartMouseListener) listeners[i]).chartMouseClicked(chartEvent);
1963        }
1964
1965    }
1966
1967    /**
1968     * Implementation of the MouseMotionListener's method.
1969     *
1970     * @param e  the event.
1971     */
1972    @Override
1973    public void mouseMoved(MouseEvent e) {
1974        Graphics2D g2 = (Graphics2D) getGraphics();
1975        g2.dispose();
1976
1977        Object[] listeners = this.chartMouseListeners.getListeners(
1978                ChartMouseListener.class);
1979        if (listeners.length == 0) {
1980            return;
1981        }
1982        Insets insets = getInsets();
1983        int x = (int) ((e.getX() - insets.left) / this.scaleX);
1984        int y = (int) ((e.getY() - insets.top) / this.scaleY);
1985
1986        ChartEntity entity = null;
1987        if (this.info != null) {
1988            EntityCollection entities = this.info.getEntityCollection();
1989            if (entities != null) {
1990                entity = entities.getEntity(x, y);
1991            }
1992        }
1993
1994        // we can only generate events if the panel's chart is not null
1995        // (see bug report 1556951)
1996        if (this.chart != null) {
1997            ChartMouseEvent event = new ChartMouseEvent(getChart(), e, entity);
1998            for (int i = listeners.length - 1; i >= 0; i -= 1) {
1999                ((ChartMouseListener) listeners[i]).chartMouseMoved(event);
2000            }
2001        }
2002
2003    }
2004
2005    /**
2006     * Zooms in on an anchor point (specified in screen coordinate space).
2007     *
2008     * @param x  the x value (in screen coordinates).
2009     * @param y  the y value (in screen coordinates).
2010     */
2011    public void zoomInBoth(double x, double y) {
2012        Plot plot = this.chart.getPlot();
2013        if (plot == null) {
2014            return;
2015        }
2016        // here we tweak the notify flag on the plot so that only
2017        // one notification happens even though we update multiple
2018        // axes...
2019        boolean savedNotify = plot.isNotify();
2020        plot.setNotify(false);
2021        zoomInDomain(x, y);
2022        zoomInRange(x, y);
2023        plot.setNotify(savedNotify);
2024    }
2025
2026    /**
2027     * Decreases the length of the domain axis, centered about the given
2028     * coordinate on the screen.  The length of the domain axis is reduced
2029     * by the value of {@link #getZoomInFactor()}.
2030     *
2031     * @param x  the x coordinate (in screen coordinates).
2032     * @param y  the y-coordinate (in screen coordinates).
2033     */
2034    public void zoomInDomain(double x, double y) {
2035        Plot plot = this.chart.getPlot();
2036        if (plot instanceof Zoomable) {
2037            // here we tweak the notify flag on the plot so that only
2038            // one notification happens even though we update multiple
2039            // axes...
2040            boolean savedNotify = plot.isNotify();
2041            plot.setNotify(false);
2042            Zoomable z = (Zoomable) plot;
2043            z.zoomDomainAxes(this.zoomInFactor, this.info.getPlotInfo(),
2044                    translateScreenToJava2D(new Point((int) x, (int) y)),
2045                    this.zoomAroundAnchor);
2046            plot.setNotify(savedNotify);
2047        }
2048    }
2049
2050    /**
2051     * Decreases the length of the range axis, centered about the given
2052     * coordinate on the screen.  The length of the range axis is reduced by
2053     * the value of {@link #getZoomInFactor()}.
2054     *
2055     * @param x  the x-coordinate (in screen coordinates).
2056     * @param y  the y coordinate (in screen coordinates).
2057     */
2058    public void zoomInRange(double x, double y) {
2059        Plot plot = this.chart.getPlot();
2060        if (plot instanceof Zoomable) {
2061            // here we tweak the notify flag on the plot so that only
2062            // one notification happens even though we update multiple
2063            // axes...
2064            boolean savedNotify = plot.isNotify();
2065            plot.setNotify(false);
2066            Zoomable z = (Zoomable) plot;
2067            z.zoomRangeAxes(this.zoomInFactor, this.info.getPlotInfo(),
2068                    translateScreenToJava2D(new Point((int) x, (int) y)),
2069                    this.zoomAroundAnchor);
2070            plot.setNotify(savedNotify);
2071        }
2072    }
2073
2074    /**
2075     * Zooms out on an anchor point (specified in screen coordinate space).
2076     *
2077     * @param x  the x value (in screen coordinates).
2078     * @param y  the y value (in screen coordinates).
2079     */
2080    public void zoomOutBoth(double x, double y) {
2081        Plot plot = this.chart.getPlot();
2082        if (plot == null) {
2083            return;
2084        }
2085        // here we tweak the notify flag on the plot so that only
2086        // one notification happens even though we update multiple
2087        // axes...
2088        boolean savedNotify = plot.isNotify();
2089        plot.setNotify(false);
2090        zoomOutDomain(x, y);
2091        zoomOutRange(x, y);
2092        plot.setNotify(savedNotify);
2093    }
2094
2095    /**
2096     * Increases the length of the domain axis, centered about the given
2097     * coordinate on the screen.  The length of the domain axis is increased
2098     * by the value of {@link #getZoomOutFactor()}.
2099     *
2100     * @param x  the x coordinate (in screen coordinates).
2101     * @param y  the y-coordinate (in screen coordinates).
2102     */
2103    public void zoomOutDomain(double x, double y) {
2104        Plot plot = this.chart.getPlot();
2105        if (plot instanceof Zoomable) {
2106            // here we tweak the notify flag on the plot so that only
2107            // one notification happens even though we update multiple
2108            // axes...
2109            boolean savedNotify = plot.isNotify();
2110            plot.setNotify(false);
2111            Zoomable z = (Zoomable) plot;
2112            z.zoomDomainAxes(this.zoomOutFactor, this.info.getPlotInfo(),
2113                    translateScreenToJava2D(new Point((int) x, (int) y)),
2114                    this.zoomAroundAnchor);
2115            plot.setNotify(savedNotify);
2116        }
2117    }
2118
2119    /**
2120     * Increases the length the range axis, centered about the given
2121     * coordinate on the screen.  The length of the range axis is increased
2122     * by the value of {@link #getZoomOutFactor()}.
2123     *
2124     * @param x  the x coordinate (in screen coordinates).
2125     * @param y  the y-coordinate (in screen coordinates).
2126     */
2127    public void zoomOutRange(double x, double y) {
2128        Plot plot = this.chart.getPlot();
2129        if (plot instanceof Zoomable) {
2130            // here we tweak the notify flag on the plot so that only
2131            // one notification happens even though we update multiple
2132            // axes...
2133            boolean savedNotify = plot.isNotify();
2134            plot.setNotify(false);
2135            Zoomable z = (Zoomable) plot;
2136            z.zoomRangeAxes(this.zoomOutFactor, this.info.getPlotInfo(),
2137                    translateScreenToJava2D(new Point((int) x, (int) y)),
2138                    this.zoomAroundAnchor);
2139            plot.setNotify(savedNotify);
2140        }
2141    }
2142
2143    /**
2144     * Zooms in on a selected region.
2145     *
2146     * @param selection  the selected region.
2147     */
2148    public void zoom(Rectangle2D selection) {
2149
2150        // get the origin of the zoom selection in the Java2D space used for
2151        // drawing the chart (that is, before any scaling to fit the panel)
2152        Point2D selectOrigin = translateScreenToJava2D(new Point(
2153                (int) Math.ceil(selection.getX()),
2154                (int) Math.ceil(selection.getY())));
2155        PlotRenderingInfo plotInfo = this.info.getPlotInfo();
2156        Rectangle2D scaledDataArea = getScreenDataArea(
2157                (int) selection.getCenterX(), (int) selection.getCenterY());
2158        if ((selection.getHeight() > 0) && (selection.getWidth() > 0)) {
2159
2160            double hLower = (selection.getMinX() - scaledDataArea.getMinX())
2161                / scaledDataArea.getWidth();
2162            double hUpper = (selection.getMaxX() - scaledDataArea.getMinX())
2163                / scaledDataArea.getWidth();
2164            double vLower = (scaledDataArea.getMaxY() - selection.getMaxY())
2165                / scaledDataArea.getHeight();
2166            double vUpper = (scaledDataArea.getMaxY() - selection.getMinY())
2167                / scaledDataArea.getHeight();
2168
2169            Plot p = this.chart.getPlot();
2170            if (p instanceof Zoomable) {
2171                // here we tweak the notify flag on the plot so that only
2172                // one notification happens even though we update multiple
2173                // axes...
2174                boolean savedNotify = p.isNotify();
2175                p.setNotify(false);
2176                Zoomable z = (Zoomable) p;
2177                if (z.getOrientation() == PlotOrientation.HORIZONTAL) {
2178                    z.zoomDomainAxes(vLower, vUpper, plotInfo, selectOrigin);
2179                    z.zoomRangeAxes(hLower, hUpper, plotInfo, selectOrigin);
2180                }
2181                else {
2182                    z.zoomDomainAxes(hLower, hUpper, plotInfo, selectOrigin);
2183                    z.zoomRangeAxes(vLower, vUpper, plotInfo, selectOrigin);
2184                }
2185                p.setNotify(savedNotify);
2186            }
2187
2188        }
2189
2190    }
2191
2192    /**
2193     * Restores the auto-range calculation on both axes.
2194     */
2195    public void restoreAutoBounds() {
2196        Plot plot = this.chart.getPlot();
2197        if (plot == null) {
2198            return;
2199        }
2200        // here we tweak the notify flag on the plot so that only
2201        // one notification happens even though we update multiple
2202        // axes...
2203        boolean savedNotify = plot.isNotify();
2204        plot.setNotify(false);
2205        restoreAutoDomainBounds();
2206        restoreAutoRangeBounds();
2207        plot.setNotify(savedNotify);
2208    }
2209
2210    /**
2211     * Restores the auto-range calculation on the domain axis.
2212     */
2213    public void restoreAutoDomainBounds() {
2214        Plot plot = this.chart.getPlot();
2215        if (plot instanceof Zoomable) {
2216            Zoomable z = (Zoomable) plot;
2217            // here we tweak the notify flag on the plot so that only
2218            // one notification happens even though we update multiple
2219            // axes...
2220            boolean savedNotify = plot.isNotify();
2221            plot.setNotify(false);
2222            // we need to guard against this.zoomPoint being null
2223            Point2D zoomPoint = this.selectionZoomStrategy.getZoomPoint();
2224            Point2D zp = zoomPoint != null ? zoomPoint : new Point();
2225            z.zoomDomainAxes(0.0, this.info.getPlotInfo(), zp);
2226            plot.setNotify(savedNotify);
2227        }
2228    }
2229
2230    /**
2231     * Restores the auto-range calculation on the range axis.
2232     */
2233    public void restoreAutoRangeBounds() {
2234        Plot plot = this.chart.getPlot();
2235        if (plot instanceof Zoomable) {
2236            Zoomable z = (Zoomable) plot;
2237            // here we tweak the notify flag on the plot so that only
2238            // one notification happens even though we update multiple
2239            // axes...
2240            boolean savedNotify = plot.isNotify();
2241            plot.setNotify(false);
2242            // we need to guard against this.zoomPoint being null
2243            Point2D zoomPoint = this.selectionZoomStrategy.getZoomPoint();
2244            Point2D zp = zoomPoint != null ? zoomPoint : new Point();
2245            z.zoomRangeAxes(0.0, this.info.getPlotInfo(), zp);
2246            plot.setNotify(savedNotify);
2247        }
2248    }
2249
2250    /**
2251     * Returns the data area for the chart (the area inside the axes) with the
2252     * current scaling applied (that is, the area as it appears on screen).
2253     *
2254     * @return The scaled data area.
2255     */
2256    public Rectangle2D getScreenDataArea() {
2257        Rectangle2D dataArea = this.info.getPlotInfo().getDataArea();
2258        Insets insets = getInsets();
2259        double x = dataArea.getX() * this.scaleX + insets.left;
2260        double y = dataArea.getY() * this.scaleY + insets.top;
2261        double w = dataArea.getWidth() * this.scaleX;
2262        double h = dataArea.getHeight() * this.scaleY;
2263        return new Rectangle2D.Double(x, y, w, h);
2264    }
2265
2266    /**
2267     * Returns the data area (the area inside the axes) for the plot or subplot,
2268     * with the current scaling applied.
2269     *
2270     * @param x  the x-coordinate (for subplot selection).
2271     * @param y  the y-coordinate (for subplot selection).
2272     *
2273     * @return The scaled data area.
2274     */
2275    public Rectangle2D getScreenDataArea(int x, int y) {
2276        PlotRenderingInfo plotInfo = this.info.getPlotInfo();
2277        Rectangle2D result;
2278        if (plotInfo.getSubplotCount() == 0) {
2279            result = getScreenDataArea();
2280        }
2281        else {
2282            // get the origin of the zoom selection in the Java2D space used for
2283            // drawing the chart (that is, before any scaling to fit the panel)
2284            Point2D selectOrigin = translateScreenToJava2D(new Point(x, y));
2285            int subplotIndex = plotInfo.getSubplotIndex(selectOrigin);
2286            if (subplotIndex == -1) {
2287                return null;
2288            }
2289            result = scale(plotInfo.getSubplotInfo(subplotIndex).getDataArea());
2290        }
2291        return result;
2292    }
2293
2294    /**
2295     * Returns the initial tooltip delay value used inside this chart panel.
2296     *
2297     * @return An integer representing the initial delay value, in milliseconds.
2298     *
2299     * @see javax.swing.ToolTipManager#getInitialDelay()
2300     */
2301    public int getInitialDelay() {
2302        return this.ownToolTipInitialDelay;
2303    }
2304
2305    /**
2306     * Returns the reshow tooltip delay value used inside this chart panel.
2307     *
2308     * @return An integer representing the reshow  delay value, in milliseconds.
2309     *
2310     * @see javax.swing.ToolTipManager#getReshowDelay()
2311     */
2312    public int getReshowDelay() {
2313        return this.ownToolTipReshowDelay;
2314    }
2315
2316    /**
2317     * Returns the dismissal tooltip delay value used inside this chart panel.
2318     *
2319     * @return An integer representing the dismissal delay value, in
2320     *         milliseconds.
2321     *
2322     * @see javax.swing.ToolTipManager#getDismissDelay()
2323     */
2324    public int getDismissDelay() {
2325        return this.ownToolTipDismissDelay;
2326    }
2327
2328    /**
2329     * Specifies the initial delay value for this chart panel.
2330     *
2331     * @param delay  the number of milliseconds to delay (after the cursor has
2332     *               paused) before displaying.
2333     *
2334     * @see javax.swing.ToolTipManager#setInitialDelay(int)
2335     */
2336    public void setInitialDelay(int delay) {
2337        this.ownToolTipInitialDelay = delay;
2338    }
2339
2340    /**
2341     * Specifies the amount of time before the user has to wait initialDelay
2342     * milliseconds before a tooltip will be shown.
2343     *
2344     * @param delay  time in milliseconds
2345     *
2346     * @see javax.swing.ToolTipManager#setReshowDelay(int)
2347     */
2348    public void setReshowDelay(int delay) {
2349        this.ownToolTipReshowDelay = delay;
2350    }
2351
2352    /**
2353     * Specifies the dismissal delay value for this chart panel.
2354     *
2355     * @param delay the number of milliseconds to delay before taking away the
2356     *              tooltip
2357     *
2358     * @see javax.swing.ToolTipManager#setDismissDelay(int)
2359     */
2360    public void setDismissDelay(int delay) {
2361        this.ownToolTipDismissDelay = delay;
2362    }
2363
2364    /**
2365     * Returns the zoom in factor.
2366     *
2367     * @return The zoom in factor.
2368     *
2369     * @see #setZoomInFactor(double)
2370     */
2371    public double getZoomInFactor() {
2372        return this.zoomInFactor;
2373    }
2374
2375    /**
2376     * Sets the zoom in factor.
2377     *
2378     * @param factor  the factor.
2379     *
2380     * @see #getZoomInFactor()
2381     */
2382    public void setZoomInFactor(double factor) {
2383        this.zoomInFactor = factor;
2384    }
2385
2386    /**
2387     * Returns the zoom out factor.
2388     *
2389     * @return The zoom out factor.
2390     *
2391     * @see #setZoomOutFactor(double)
2392     */
2393    public double getZoomOutFactor() {
2394        return this.zoomOutFactor;
2395    }
2396
2397    /**
2398     * Sets the zoom out factor.
2399     *
2400     * @param factor  the factor.
2401     *
2402     * @see #getZoomOutFactor()
2403     */
2404    public void setZoomOutFactor(double factor) {
2405        this.zoomOutFactor = factor;
2406    }
2407
2408
2409    /**
2410     * Displays a dialog that allows the user to edit the properties for the
2411     * current chart.
2412     */
2413    public void doEditChartProperties() {
2414
2415        ChartEditor editor = ChartEditorManager.getChartEditor(this.chart);
2416        int result = JOptionPane.showConfirmDialog(this, editor,
2417                localizationResources.getString("Chart_Properties"),
2418                JOptionPane.OK_CANCEL_OPTION, JOptionPane.PLAIN_MESSAGE);
2419        if (result == JOptionPane.OK_OPTION) {
2420            editor.updateChart(this.chart);
2421        }
2422
2423    }
2424
2425    /**
2426     * Copies the current chart to the system clipboard.
2427     */
2428    public void doCopy() {
2429        Clipboard systemClipboard
2430                = Toolkit.getDefaultToolkit().getSystemClipboard();
2431        Insets insets = getInsets();
2432        int w = getWidth() - insets.left - insets.right;
2433        int h = getHeight() - insets.top - insets.bottom;
2434        ChartTransferable selection = new ChartTransferable(this.chart, w, h,
2435                getMinimumDrawWidth(), getMinimumDrawHeight(),
2436                getMaximumDrawWidth(), getMaximumDrawHeight(), true);
2437        systemClipboard.setContents(selection, null);
2438    }
2439
2440    /**
2441     * Opens a file chooser and gives the user an opportunity to save the chart
2442     * in PNG format.
2443     *
2444     * @throws IOException if there is an I/O error.
2445     */
2446    public void doSaveAs() throws IOException {
2447        doSaveAs(-1, -1);
2448    }
2449
2450    /**
2451     * Opens a file chooser and gives the user an opportunity to save the chart
2452     * in PNG format.
2453     *
2454     * @param w  the width for the saved image (if less than or equal to zero,
2455     *      the panel width will be used);
2456     * @param h  the height for the PNG image (if less than or equal to zero,
2457     *      the panel height will be used);
2458     *
2459     * @throws IOException if there is an I/O error.
2460     */
2461    public void doSaveAs(int w, int h) throws IOException {
2462        JFileChooser fileChooser = new JFileChooser();
2463        fileChooser.setCurrentDirectory(this.defaultDirectoryForSaveAs);
2464        FileNameExtensionFilter filter = new FileNameExtensionFilter(
2465                    localizationResources.getString("PNG_Image_Files"), "png");
2466        fileChooser.addChoosableFileFilter(filter);
2467        fileChooser.setFileFilter(filter);
2468
2469        int option = fileChooser.showSaveDialog(this);
2470        if (option == JFileChooser.APPROVE_OPTION) {
2471            String filename = fileChooser.getSelectedFile().getPath();
2472            if (isEnforceFileExtensions()) {
2473                if (!filename.endsWith(".png")) {
2474                    filename = filename + ".png";
2475                }
2476            }
2477            if (w <= 0) {
2478                w = getWidth();
2479            }
2480            if (h <= 0) {
2481                h = getHeight();
2482            }
2483            ChartUtils.saveChartAsPNG(new File(filename), this.chart, w, h);
2484        }
2485    }
2486    
2487    /**
2488     * Saves the chart in SVG format (a filechooser will be displayed so that
2489     * the user can specify the filename).  Note that this method only works
2490     * if the JFreeSVG library is on the classpath...if this library is not 
2491     * present, the method will fail.
2492     *
2493     * @param f  the file.
2494     *
2495     * @throws IOException if there is an exception.
2496     */
2497    protected void saveAsSVG(File f) throws IOException {
2498        File file = f;
2499        if (file == null) {
2500            JFileChooser fileChooser = new JFileChooser();
2501            fileChooser.setCurrentDirectory(this.defaultDirectoryForSaveAs);
2502            FileNameExtensionFilter filter = new FileNameExtensionFilter(
2503                    localizationResources.getString("SVG_Files"), "svg");
2504            fileChooser.addChoosableFileFilter(filter);
2505            fileChooser.setFileFilter(filter);
2506
2507            int option = fileChooser.showSaveDialog(this);
2508            if (option == JFileChooser.APPROVE_OPTION) {
2509                String filename = fileChooser.getSelectedFile().getPath();
2510                if (isEnforceFileExtensions()) {
2511                    if (!filename.endsWith(".svg")) {
2512                        filename = filename + ".svg";
2513                    }
2514                }
2515                file = new File(filename);
2516                if (file.exists()) {
2517                    String fileExists = localizationResources.getString(
2518                            "FILE_EXISTS_CONFIRM_OVERWRITE");
2519                    int response = JOptionPane.showConfirmDialog(this, 
2520                            fileExists,
2521                            localizationResources.getString("Save_as_SVG"),
2522                            JOptionPane.OK_CANCEL_OPTION);
2523                    if (response == JOptionPane.CANCEL_OPTION) {
2524                        file = null;
2525                    }
2526                }
2527            }
2528        }
2529        
2530        if (file != null) {
2531            // use reflection to get the SVG string
2532            String svg = generateSVG(getWidth(), getHeight());
2533            BufferedWriter writer = null;
2534            Exception originalException = null;
2535            try {
2536                writer = new BufferedWriter(new FileWriter(file));
2537                writer.write("<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\" \"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\">\n");
2538                writer.write(svg + "\n");
2539                writer.flush();
2540            } catch (Exception e) {
2541                originalException = e;
2542            }
2543            try {
2544                if (writer != null) {
2545                    writer.close();
2546                }
2547            } catch (IOException ex) {
2548                RuntimeException th = new RuntimeException(ex);
2549                if (originalException != null)
2550                    th.addSuppressed(originalException);
2551                throw th;
2552            }
2553        }
2554    }
2555    
2556    /**
2557     * Generates a string containing a rendering of the chart in SVG format.
2558     * This feature is only supported if the JFreeSVG library is included on 
2559     * the classpath.
2560     * 
2561     * @param width  the width.
2562     * @param height  the height.
2563     *
2564     * @return A string containing an SVG element for the current chart, or
2565     *     {@code null} if there is a problem with the method invocation
2566     *     by reflection.
2567     */
2568    protected String generateSVG(int width, int height) {
2569        Graphics2D g2 = createSVGGraphics2D(width, height);
2570        if (g2 == null) {
2571            throw new IllegalStateException("JFreeSVG library is not present.");
2572        }
2573        // we suppress shadow generation, because SVG is a vector format and
2574        // the shadow effect is applied via bitmap effects...
2575        g2.setRenderingHint(JFreeChart.KEY_SUPPRESS_SHADOW_GENERATION, true);
2576        String svg = null;
2577        Rectangle2D drawArea = new Rectangle2D.Double(0, 0, width, height);
2578        this.chart.draw(g2, drawArea);
2579        try {
2580            Method m = g2.getClass().getMethod("getSVGElement");
2581            svg = (String) m.invoke(g2);
2582        } catch (NoSuchMethodException | SecurityException | IllegalAccessException |
2583                IllegalArgumentException | InvocationTargetException e) {
2584            // null will be returned
2585        }
2586        return svg;
2587    }
2588
2589    /**
2590     * Creates an {@code SVGGraphics2D} instance (from JFreeSVG) using reflection.
2591     * If JFreeSVG is not on the classpath, this method returns {@code null}.
2592     *
2593     * @param w  the width.
2594     * @param h  the height.
2595     *
2596     * @return An {@code SVGGraphics2D} instance or {@code null}.
2597     */
2598    protected Graphics2D createSVGGraphics2D(int w, int h) {
2599        try {
2600            Class<?> svgGraphics2d = Class.forName("org.jfree.graphics2d.svg.SVGGraphics2D");
2601            Constructor<?> ctor = svgGraphics2d.getConstructor(int.class, int.class);
2602            return (Graphics2D) ctor.newInstance(w, h);
2603        } catch (ClassNotFoundException | NoSuchMethodException | SecurityException | InstantiationException |
2604                IllegalAccessException | IllegalArgumentException | InvocationTargetException ex) {
2605            return null;
2606        }
2607    }
2608
2609    /**
2610     * Saves the chart in PDF format (a filechooser will be displayed so that
2611     * the user can specify the filename).  Note that this method only works
2612     * if the OrsonPDF library is on the classpath...if this library is not
2613     * present, the method will fail.
2614     *
2615     * @param f  the file.
2616     */
2617    protected void saveAsPDF(File f) {
2618        File file = f;
2619        if (file == null) {
2620            JFileChooser fileChooser = new JFileChooser();
2621            fileChooser.setCurrentDirectory(this.defaultDirectoryForSaveAs);
2622            FileNameExtensionFilter filter = new FileNameExtensionFilter(
2623                    localizationResources.getString("PDF_Files"), "pdf");
2624            fileChooser.addChoosableFileFilter(filter);
2625            fileChooser.setFileFilter(filter);
2626
2627            int option = fileChooser.showSaveDialog(this);
2628            if (option == JFileChooser.APPROVE_OPTION) {
2629                String filename = fileChooser.getSelectedFile().getPath();
2630                if (isEnforceFileExtensions()) {
2631                    if (!filename.endsWith(".pdf")) {
2632                        filename = filename + ".pdf";
2633                    }
2634                }
2635                file = new File(filename);
2636                if (file.exists()) {
2637                    String fileExists = localizationResources.getString(
2638                            "FILE_EXISTS_CONFIRM_OVERWRITE");
2639                    int response = JOptionPane.showConfirmDialog(this, 
2640                            fileExists,
2641                            localizationResources.getString("Save_as_PDF"),
2642                            JOptionPane.OK_CANCEL_OPTION);
2643                    if (response == JOptionPane.CANCEL_OPTION) {
2644                        file = null;
2645                    }
2646                }
2647            }
2648        }
2649        
2650        if (file != null) {
2651            writeAsPDF(file, getWidth(), getHeight());
2652        }
2653    }
2654
2655    /**
2656     * Writes the current chart to the specified file in PDF format.  This 
2657     * will only work when the OrsonPDF library is found on the classpath.
2658     * Reflection is used to ensure there is no compile-time dependency on
2659     * OrsonPDF (which is non-free software).
2660     * 
2661     * @param file  the output file ({@code null} not permitted).
2662     * @param w  the chart width.
2663     * @param h  the chart height.
2664     */
2665    private void writeAsPDF(File file, int w, int h) {
2666        if (!ChartUtils.isOrsonPDFAvailable()) {
2667            throw new IllegalStateException(
2668                    "OrsonPDF is not present on the classpath.");
2669        }
2670        Args.nullNotPermitted(file, "file");
2671        try {
2672            Class<?> pdfDocClass = Class.forName("com.orsonpdf.PDFDocument");
2673            Object pdfDoc = pdfDocClass.getDeclaredConstructor().newInstance();
2674            Method m = pdfDocClass.getMethod("createPage", Rectangle2D.class);
2675            Rectangle2D rect = new Rectangle(w, h);
2676            Object page = m.invoke(pdfDoc, rect);
2677            Method m2 = page.getClass().getMethod("getGraphics2D");
2678            Graphics2D g2 = (Graphics2D) m2.invoke(page);
2679            // we suppress shadow generation, because PDF is a vector format and
2680            // the shadow effect is applied via bitmap effects...
2681            g2.setRenderingHint(JFreeChart.KEY_SUPPRESS_SHADOW_GENERATION, true);
2682            Rectangle2D drawArea = new Rectangle2D.Double(0, 0, w, h);
2683            this.chart.draw(g2, drawArea);
2684            Method m3 = pdfDocClass.getMethod("writeToFile", File.class);
2685            m3.invoke(pdfDoc, file);
2686        } catch (ClassNotFoundException | InstantiationException | IllegalAccessException |
2687                NoSuchMethodException | SecurityException | IllegalArgumentException |
2688                InvocationTargetException ex) {
2689            throw new RuntimeException(ex);
2690        }
2691    }
2692
2693    /**
2694     * Creates a print job for the chart.
2695     */
2696    public void createChartPrintJob() {
2697        PrinterJob job = PrinterJob.getPrinterJob();
2698        PageFormat pf = job.defaultPage();
2699        PageFormat pf2 = job.pageDialog(pf);
2700        if (pf2 != pf) {
2701            job.setPrintable(this, pf2);
2702            if (job.printDialog()) {
2703                try {
2704                    job.print();
2705                }
2706                catch (PrinterException e) {
2707                    JOptionPane.showMessageDialog(this, e);
2708                }
2709            }
2710        }
2711    }
2712
2713    /**
2714     * Prints the chart on a single page.
2715     *
2716     * @param g  the graphics context.
2717     * @param pf  the page format to use.
2718     * @param pageIndex  the index of the page. If not {@code 0}, nothing
2719     *                   gets printed.
2720     *
2721     * @return The result of printing.
2722     */
2723    @Override
2724    public int print(Graphics g, PageFormat pf, int pageIndex) {
2725
2726        if (pageIndex != 0) {
2727            return NO_SUCH_PAGE;
2728        }
2729        Graphics2D g2 = (Graphics2D) g;
2730        double x = pf.getImageableX();
2731        double y = pf.getImageableY();
2732        double w = pf.getImageableWidth();
2733        double h = pf.getImageableHeight();
2734        this.chart.draw(g2, new Rectangle2D.Double(x, y, w, h), this.anchor,
2735                null);
2736        return PAGE_EXISTS;
2737
2738    }
2739
2740    /**
2741     * Adds a listener to the list of objects listening for chart mouse events.
2742     *
2743     * @param listener  the listener ({@code null} not permitted).
2744     */
2745    public void addChartMouseListener(ChartMouseListener listener) {
2746        Args.nullNotPermitted(listener, "listener");
2747        this.chartMouseListeners.add(ChartMouseListener.class, listener);
2748    }
2749
2750    /**
2751     * Removes a listener from the list of objects listening for chart mouse
2752     * events.
2753     *
2754     * @param listener  the listener.
2755     */
2756    public void removeChartMouseListener(ChartMouseListener listener) {
2757        this.chartMouseListeners.remove(ChartMouseListener.class, listener);
2758    }
2759
2760    /**
2761     * Returns an array of the listeners of the given type registered with the
2762     * panel.
2763     *
2764     * @param listenerType  the listener type.
2765     *
2766     * @return An array of listeners.
2767     */
2768    @Override
2769    public <T extends EventListener> T[] getListeners(Class<T> listenerType) {
2770        if (listenerType == ChartMouseListener.class) {
2771            // fetch listeners from local storage
2772            return this.chartMouseListeners.getListeners(listenerType);
2773        }
2774        else {
2775            return super.getListeners(listenerType);
2776        }
2777    }
2778
2779    /**
2780     * Creates a popup menu for the panel.  This method includes code that
2781     * auto-detects JFreeSVG and OrsonPDF (via reflection) and, if they are
2782     * present (and the {@code save} argument is {@code true}, adds a menu item
2783     * for each.
2784     *
2785     * @param properties  include a menu item for the chart property editor.
2786     * @param copy include a menu item for copying to the clipboard.
2787     * @param save  include one or more menu items for saving the chart to
2788     *     supported image formats.
2789     * @param print  include a menu item for printing the chart.
2790     * @param zoom  include menu items for zooming.
2791     *
2792     * @return The popup menu.
2793     */
2794    protected JPopupMenu createPopupMenu(boolean properties,
2795            boolean copy, boolean save, boolean print, boolean zoom) {
2796
2797        JPopupMenu result = new JPopupMenu(localizationResources.getString("Chart") + ":");
2798        boolean separator = false;
2799
2800        if (properties) {
2801            JMenuItem propertiesItem = new JMenuItem(
2802                    localizationResources.getString("Properties..."));
2803            propertiesItem.setActionCommand(PROPERTIES_COMMAND);
2804            propertiesItem.addActionListener(this);
2805            result.add(propertiesItem);
2806            separator = true;
2807        }
2808
2809        if (copy) {
2810            if (separator) {
2811                result.addSeparator();
2812            }
2813            JMenuItem copyItem = new JMenuItem(
2814                    localizationResources.getString("Copy"));
2815            copyItem.setActionCommand(COPY_COMMAND);
2816            copyItem.addActionListener(this);
2817            result.add(copyItem);
2818            separator = !save;
2819        }
2820
2821        if (save) {
2822            if (separator) {
2823                result.addSeparator();
2824            }
2825
2826            JMenu saveSubMenu = new JMenu(localizationResources.getString("Save_as"));
2827
2828            // PNG - current res
2829            {
2830                JMenuItem pngItem = new JMenuItem(localizationResources.getString(
2831                        "PNG..."));
2832                pngItem.setActionCommand(SAVE_AS_PNG_COMMAND);
2833                pngItem.addActionListener(this);
2834                saveSubMenu.add(pngItem);
2835
2836            }
2837
2838            // PNG - screen res
2839            {
2840                final Dimension ss = Toolkit.getDefaultToolkit().getScreenSize();
2841                final String pngName = "PNG ("+ss.width+"x"+ss.height+") ...";
2842                JMenuItem pngItem = new JMenuItem(pngName);
2843                pngItem.setActionCommand(SAVE_AS_PNG_SIZE_COMMAND);
2844                pngItem.addActionListener(this);
2845                saveSubMenu.add(pngItem);
2846            }
2847            
2848            if (ChartUtils.isJFreeSVGAvailable()) {
2849                JMenuItem svgItem = new JMenuItem(localizationResources.getString(
2850                        "SVG..."));
2851                svgItem.setActionCommand(SAVE_AS_SVG_COMMAND);
2852                svgItem.addActionListener(this);
2853                saveSubMenu.add(svgItem);                
2854            }
2855            
2856            if (ChartUtils.isOrsonPDFAvailable()) {
2857                JMenuItem pdfItem = new JMenuItem(
2858                        localizationResources.getString("PDF..."));
2859                pdfItem.setActionCommand(SAVE_AS_PDF_COMMAND);
2860                pdfItem.addActionListener(this);
2861                saveSubMenu.add(pdfItem);
2862            }
2863            result.add(saveSubMenu);
2864            separator = true;
2865        }
2866
2867        if (print) {
2868            if (separator) {
2869                result.addSeparator();
2870            }
2871            JMenuItem printItem = new JMenuItem(
2872                    localizationResources.getString("Print..."));
2873            printItem.setActionCommand(PRINT_COMMAND);
2874            printItem.addActionListener(this);
2875            result.add(printItem);
2876            separator = true;
2877        }
2878
2879        if (zoom) {
2880            if (separator) {
2881                result.addSeparator();
2882            }
2883
2884            JMenu zoomInMenu = new JMenu(
2885                    localizationResources.getString("Zoom_In"));
2886
2887            this.zoomInBothMenuItem = new JMenuItem(
2888                    localizationResources.getString("All_Axes"));
2889            this.zoomInBothMenuItem.setActionCommand(ZOOM_IN_BOTH_COMMAND);
2890            this.zoomInBothMenuItem.addActionListener(this);
2891            zoomInMenu.add(this.zoomInBothMenuItem);
2892
2893            zoomInMenu.addSeparator();
2894
2895            this.zoomInDomainMenuItem = new JMenuItem(
2896                    localizationResources.getString("Domain_Axis"));
2897            this.zoomInDomainMenuItem.setActionCommand(ZOOM_IN_DOMAIN_COMMAND);
2898            this.zoomInDomainMenuItem.addActionListener(this);
2899            zoomInMenu.add(this.zoomInDomainMenuItem);
2900
2901            this.zoomInRangeMenuItem = new JMenuItem(
2902                    localizationResources.getString("Range_Axis"));
2903            this.zoomInRangeMenuItem.setActionCommand(ZOOM_IN_RANGE_COMMAND);
2904            this.zoomInRangeMenuItem.addActionListener(this);
2905            zoomInMenu.add(this.zoomInRangeMenuItem);
2906
2907            result.add(zoomInMenu);
2908
2909            JMenu zoomOutMenu = new JMenu(
2910                    localizationResources.getString("Zoom_Out"));
2911
2912            this.zoomOutBothMenuItem = new JMenuItem(
2913                    localizationResources.getString("All_Axes"));
2914            this.zoomOutBothMenuItem.setActionCommand(ZOOM_OUT_BOTH_COMMAND);
2915            this.zoomOutBothMenuItem.addActionListener(this);
2916            zoomOutMenu.add(this.zoomOutBothMenuItem);
2917
2918            zoomOutMenu.addSeparator();
2919
2920            this.zoomOutDomainMenuItem = new JMenuItem(
2921                    localizationResources.getString("Domain_Axis"));
2922            this.zoomOutDomainMenuItem.setActionCommand(
2923                    ZOOM_OUT_DOMAIN_COMMAND);
2924            this.zoomOutDomainMenuItem.addActionListener(this);
2925            zoomOutMenu.add(this.zoomOutDomainMenuItem);
2926
2927            this.zoomOutRangeMenuItem = new JMenuItem(
2928                    localizationResources.getString("Range_Axis"));
2929            this.zoomOutRangeMenuItem.setActionCommand(ZOOM_OUT_RANGE_COMMAND);
2930            this.zoomOutRangeMenuItem.addActionListener(this);
2931            zoomOutMenu.add(this.zoomOutRangeMenuItem);
2932
2933            result.add(zoomOutMenu);
2934
2935            JMenu autoRangeMenu = new JMenu(
2936                    localizationResources.getString("Auto_Range"));
2937
2938            this.zoomResetBothMenuItem = new JMenuItem(
2939                    localizationResources.getString("All_Axes"));
2940            this.zoomResetBothMenuItem.setActionCommand(
2941                    ZOOM_RESET_BOTH_COMMAND);
2942            this.zoomResetBothMenuItem.addActionListener(this);
2943            autoRangeMenu.add(this.zoomResetBothMenuItem);
2944
2945            autoRangeMenu.addSeparator();
2946            this.zoomResetDomainMenuItem = new JMenuItem(
2947                    localizationResources.getString("Domain_Axis"));
2948            this.zoomResetDomainMenuItem.setActionCommand(
2949                    ZOOM_RESET_DOMAIN_COMMAND);
2950            this.zoomResetDomainMenuItem.addActionListener(this);
2951            autoRangeMenu.add(this.zoomResetDomainMenuItem);
2952
2953            this.zoomResetRangeMenuItem = new JMenuItem(
2954                    localizationResources.getString("Range_Axis"));
2955            this.zoomResetRangeMenuItem.setActionCommand(
2956                    ZOOM_RESET_RANGE_COMMAND);
2957            this.zoomResetRangeMenuItem.addActionListener(this);
2958            autoRangeMenu.add(this.zoomResetRangeMenuItem);
2959
2960            result.addSeparator();
2961            result.add(autoRangeMenu);
2962
2963        }
2964
2965        return result;
2966
2967    }
2968
2969    /**
2970     * The idea is to modify the zooming options depending on the type of chart
2971     * being displayed by the panel.
2972     *
2973     * @param x  horizontal position of the popup.
2974     * @param y  vertical position of the popup.
2975     */
2976    protected void displayPopupMenu(int x, int y) {
2977
2978        if (this.popup == null) {
2979            return;
2980        }
2981
2982        // go through each zoom menu item and decide whether or not to
2983        // enable it...
2984        boolean isDomainZoomable = false;
2985        boolean isRangeZoomable = false;
2986        Plot plot = (this.chart != null ? this.chart.getPlot() : null);
2987        if (plot instanceof Zoomable) {
2988            Zoomable z = (Zoomable) plot;
2989            isDomainZoomable = z.isDomainZoomable();
2990            isRangeZoomable = z.isRangeZoomable();
2991        }
2992
2993        if (this.zoomInDomainMenuItem != null) {
2994            this.zoomInDomainMenuItem.setEnabled(isDomainZoomable);
2995        }
2996        if (this.zoomOutDomainMenuItem != null) {
2997            this.zoomOutDomainMenuItem.setEnabled(isDomainZoomable);
2998        }
2999        if (this.zoomResetDomainMenuItem != null) {
3000            this.zoomResetDomainMenuItem.setEnabled(isDomainZoomable);
3001        }
3002
3003        if (this.zoomInRangeMenuItem != null) {
3004            this.zoomInRangeMenuItem.setEnabled(isRangeZoomable);
3005        }
3006        if (this.zoomOutRangeMenuItem != null) {
3007            this.zoomOutRangeMenuItem.setEnabled(isRangeZoomable);
3008        }
3009
3010        if (this.zoomResetRangeMenuItem != null) {
3011            this.zoomResetRangeMenuItem.setEnabled(isRangeZoomable);
3012        }
3013
3014        if (this.zoomInBothMenuItem != null) {
3015            this.zoomInBothMenuItem.setEnabled(isDomainZoomable
3016                    && isRangeZoomable);
3017        }
3018        if (this.zoomOutBothMenuItem != null) {
3019            this.zoomOutBothMenuItem.setEnabled(isDomainZoomable
3020                    && isRangeZoomable);
3021        }
3022        if (this.zoomResetBothMenuItem != null) {
3023            this.zoomResetBothMenuItem.setEnabled(isDomainZoomable
3024                    && isRangeZoomable);
3025        }
3026
3027        this.popup.show(this, x, y);
3028
3029    }
3030
3031    /**
3032     * Updates the UI for a LookAndFeel change.
3033     */
3034    @Override
3035    public void updateUI() {
3036        // here we need to update the UI for the popup menu, if the panel
3037        // has one...
3038        if (this.popup != null) {
3039            SwingUtilities.updateComponentTreeUI(this.popup);
3040        }
3041        super.updateUI();
3042    }
3043
3044    /**
3045     * Provides serialization support.
3046     *
3047     * @param stream  the output stream.
3048     *
3049     * @throws IOException  if there is an I/O error.
3050     */
3051    protected void writeObject(ObjectOutputStream stream) throws IOException {
3052        stream.defaultWriteObject();
3053    }
3054
3055    /**
3056     * Provides serialization support.
3057     *
3058     * @param stream  the input stream.
3059     *
3060     * @throws IOException  if there is an I/O error.
3061     * @throws ClassNotFoundException  if there is a classpath problem.
3062     */
3063    protected void readObject(ObjectInputStream stream)
3064        throws IOException, ClassNotFoundException {
3065        stream.defaultReadObject();
3066
3067        // we create a new but empty chartMouseListeners list
3068        this.chartMouseListeners = new EventListenerList();
3069
3070        // register as a listener with sub-components...
3071        if (this.chart != null) {
3072            this.chart.addChangeListener(this);
3073        }
3074
3075    }
3076
3077}
3078