001/**
002 * AbstractPlotWindow -- Partial implementation of a plot window.
003 *
004 * Copyright (C) 2011-2017, by Joseph A. Huwaldt. All rights reserved.
005 *
006 * This library is free software; you can redistribute it and/or modify it under the terms
007 * of the GNU Lesser General Public License as published by the Free Software Foundation;
008 * either version 2.1 of the License, or (at your option) any later version.
009 *
010 * This library is distributed in the hope that it will be useful, but WITHOUT ANY
011 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
012 * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
013 *
014 * You should have received a copy of the GNU Lesser General Public License along with
015 * this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place -
016 * Suite 330, Boston, MA 02111-1307, USA. Or visit: http://www.gnu.org/licenses/lgpl.html
017 */
018package geomss.ui;
019
020import geomss.app.GeomSSGUI;
021import jahuwaldt.io.ExtFilenameFilter;
022import jahuwaldt.swing.AppUtilities;
023import jahuwaldt.swing.ImageSelection;
024import jahuwaldt.swing.MDIApplication;
025import java.awt.Dimension;
026import java.awt.Graphics2D;
027import java.awt.Image;
028import java.awt.Toolkit;
029import java.awt.event.ActionEvent;
030import java.awt.event.ActionListener;
031import java.awt.event.WindowEvent;
032import java.awt.geom.Rectangle2D;
033import java.awt.print.PageFormat;
034import java.awt.print.PrinterException;
035import java.awt.print.PrinterJob;
036import java.io.*;
037import java.lang.reflect.Constructor;
038import java.lang.reflect.InvocationTargetException;
039import java.lang.reflect.Method;
040import java.text.MessageFormat;
041import static java.util.Objects.isNull;
042import static java.util.Objects.nonNull;
043import java.util.ResourceBundle;
044import java.util.logging.Level;
045import java.util.logging.Logger;
046import javax.swing.*;
047import net.roydesign.app.AboutJMenuItem;
048import net.roydesign.app.QuitJMenuItem;
049import org.jfree.chart.ChartPanel;
050import org.jfree.chart.ChartUtilities;
051import org.jfree.chart.JFreeChart;
052
053/**
054 * A partial implementation of a window that displays a plot using JFreeChart.
055 *
056 * <p> Modified by: Joseph A. Huwaldt </p>
057 *
058 * @author Joseph A. Huwaldt Date: September 3, 2011
059 * @version January 31, 2017
060 */
061public abstract class AbstractPlotWindow extends JFrame {
062
063    private static final long serialVersionUID = 1L;
064
065    //  Application frame width & height.
066    private static final short FRAME_WIDTH = 800;
067    private static final short FRAME_HEIGHT = 600;
068    
069    //  Flag indicating if we have SVG output capability.
070    private static final boolean HAS_SVG = nonNull(createSVGGraphics2D(10, 10));
071    
072    /**
073     * The resource bundle for this application containing the application strings.
074     */
075    protected final ResourceBundle RB;
076    
077    //  The name of the plot.
078    private final String _dataName;
079
080    //  The page format used for printing.
081    private PageFormat _pf = null;
082
083    //  The panel used to display the chart.
084    private ChartPanel _chartPanel;
085
086    /**
087     * Construct a new AbstractPlotWindow with the given parameters.
088     *
089     * @param title       The title for the window.
090     * @param dataName    Name of the data being plotted (possibly the name of the file it
091     *                    came from).
092     */
093    @SuppressWarnings({"LeakingThisInConstructor", "OverridableMethodCallInConstructor"})
094    public AbstractPlotWindow(String title, String dataName) {
095        super(title);
096
097        MDIApplication app = MDIApplication.getInstance();
098        RB = app.getResourceBundle();
099        app.addWindow(this);
100        
101        _dataName = dataName;
102
103        setResizable(true);
104        setSize(FRAME_WIDTH, FRAME_HEIGHT);
105
106        //  Position the window so that each new window can be seen.
107        AppUtilities.positionWindow(this, FRAME_WIDTH, FRAME_HEIGHT);
108
109        //  Have the window dispose of itself when it closes.
110        setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
111
112        try {
113
114            //  Create a menu bar for this window.
115            JMenuBar menuBar = createMenuBar();
116            this.setJMenuBar(menuBar);
117
118        } catch (NoSuchMethodException ex) {
119            // Shouldn't happen if the menu bar methods all exist.
120            Logger.getLogger(AbstractPlotWindow.class.getName()).log(Level.WARNING, "", ex);
121        }
122
123    }
124
125    /**
126     * Sets the title for this window to the specified string. Also notifies the main
127     * application class so that the "Windows" menu gets updated too.
128     *
129     * @param title The title to be displayed in the frame's border. A null value is
130     *              treated as an empty string, "".
131     */
132    @Override
133    public void setTitle(String title) {
134        super.setTitle(title);
135        GeomSSGUI.getInstance().windowTitleChanged(this);
136    }
137
138    /**
139     * Return the name to use for the data in this window.
140     *
141     * @return The name to use for the data in this window.
142     */
143    public String getDataName() {
144        return _dataName;
145    }
146
147    /**
148     * Initializes the menus associated with this window.
149     */
150    private JMenuBar createMenuBar() throws NoSuchMethodException {
151        JMenuBar menuBar = new JMenuBar();
152
153        // Set up the file menu.
154        int row = 0;
155        String[][] menuStrings = new String[11][3];
156        menuStrings[row][0] = RB.getString("newItemText");
157        menuStrings[row][1] = RB.getString("newItemKey");
158        menuStrings[row][2] = null;
159        ++row;
160        menuStrings[row][0] = RB.getString("openItemText");
161        menuStrings[row][1] = RB.getString("openItemKey");
162        menuStrings[row][2] = null;
163        ++row;
164        ++row;  //  Blank line
165        menuStrings[row][0] = RB.getString("closeItemText");
166        menuStrings[row][1] = RB.getString("closeItemKey");
167        menuStrings[row][2] = "handleClose";
168        ++row;
169        menuStrings[row][0] = RB.getString("saveItemText");
170        menuStrings[row][1] = RB.getString("saveItemKey");
171        menuStrings[row][2] = null;
172        ++row;
173        menuStrings[row][0] = RB.getString("saveAsPNGItemText");
174        menuStrings[row][1] = null;
175        menuStrings[row][2] = "handleSaveAsPNG";
176        ++row;
177        if (HAS_SVG) {
178            menuStrings[row][0] = RB.getString("saveAsSVGItemText");
179            menuStrings[row][1] = null;
180            menuStrings[row][2] = "handleSaveAsSVG";
181            ++row;
182        }
183        int exportAsMItem = row;
184        menuStrings[row][0] = RB.getString("exportAsItemText");
185        menuStrings[row][1] = null;
186        menuStrings[row][2] = null;
187        ++row;
188        ++row;  //  Blank line
189        menuStrings[row][0] = RB.getString("pageSetupItemText");
190        menuStrings[row][1] = null;
191        menuStrings[row][2] = "handlePageSetup";
192        ++row;
193        menuStrings[row][0] = RB.getString("printItemText");
194        menuStrings[row][1] = RB.getString("printItemKey");
195        menuStrings[row][2] = "handlePrint";
196        JMenu menu = AppUtilities.buildMenu(this, RB.getString("fileMenuText"), menuStrings);
197        menuBar.add(menu);
198
199        //  Add the Export As sub-menu.
200        JMenu exportMenu = buildExportMenu(menu.getItem(exportAsMItem).getText());
201        if (nonNull(exportMenu)) {
202            menu.remove(exportAsMItem);
203            menu.add(exportMenu, exportAsMItem);
204        }
205
206        //  Add a Quit menu item.
207        MDIApplication app = MDIApplication.getInstance();
208        QuitJMenuItem quit = app.getQuitJMenuItem();
209        quit.addActionListener(new ActionListener() {
210            @Override
211            public void actionPerformed(ActionEvent e) {
212                GeomSSGUI.getInstance().handleQuit(e);
213            }
214        });
215        if (!QuitJMenuItem.isAutomaticallyPresent()) {
216            menu.addSeparator();
217            menu.add(quit);
218        }
219
220        // Set up the edit menu.
221        row = 0;
222        menuStrings = new String[6][3];
223        menuStrings[row][0] = RB.getString("undoItemText");
224        menuStrings[row][1] = RB.getString("undoItemKey");
225        menuStrings[row][2] = null;
226        ++row;
227        menuStrings[row][0] = RB.getString("redoItemText");
228        menuStrings[row][1] = null;
229        menuStrings[row][2] = null;
230        ++row;
231        ++row;  //  Blank line.
232        menuStrings[row][0] = RB.getString("cutItemText");
233        menuStrings[row][1] = RB.getString("cutItemKey");
234        menuStrings[row][2] = null;
235        ++row;
236        menuStrings[row][0] = RB.getString("copyItemText");
237        menuStrings[row][1] = RB.getString("copyItemKey");
238        menuStrings[row][2] = "handleCopy";
239        ++row;
240        menuStrings[row][0] = RB.getString("pasteItemText");
241        menuStrings[row][1] = RB.getString("pasteItemKey");
242        menuStrings[row][2] = null;
243        menu = AppUtilities.buildMenu(this, RB.getString("editMenuText"), menuStrings);
244        menuBar.add(menu);
245
246        //  Create a Window's menu.
247        menu = app.newWindowsMenu(RB.getString("windowsMenuText"));
248        menuBar.add(menu);
249
250        //  Create an about menu item.
251        AboutJMenuItem about = app.createAboutMenuItem();
252
253        //  Create a "Help" menu for non-MacOS platforms.
254        if (!AboutJMenuItem.isAutomaticallyPresent()) {
255            //  Create Help menu.
256            menu = new JMenu(RB.getString("helpMenuText"));
257            menuBar.add(menu);
258
259            //  Add the "About" item to the Help menu.
260            menu.add(about);
261        }
262
263        return menuBar;
264    }
265
266    /**
267     * Method that will build an "Export" menu containing a list of all the objects that
268     * are capable of writing to files.
269     *
270     * @param title The title for the menu that is created.
271     * @return A JMenu instance containing a list of all the objects that can write to
272     *         files in various formats. If there are no appropriate readers available,
273     *         null is returned.
274     * @throws java.lang.NoSuchMethodException
275     */
276    protected abstract JMenu buildExportMenu(String title) throws NoSuchMethodException;
277
278    /**
279     * Return the panel used to display the chart (may be <code>null</code> if the chart
280     * has not been created yet).
281     *
282     * @return The panel used to display the chart.
283     */
284    public ChartPanel getChartPanel() {
285        return _chartPanel;
286    }
287
288    /**
289     * Creates a panel that contains the chart of the data and modifies the chart pop-up
290     * menu to provide custom behavior.
291     *
292     * @param chart  The chart to add to the chart panel.
293     * @param target The object that receives the action events. Must have the following
294     *               methods defined: "handlePrint(ActionEvent e)",
295     *               "handlePageSetup(ActionEvent e)", "handleSaveAsPNG(ActionEvent e)",
296     *               and "handleSaveAsSVG(ActionEvent e)".
297     * @throws java.lang.NoSuchMethodException
298     * @see getChartPanel
299     */
300    protected void createChartPanel(JFreeChart chart, Object target) throws NoSuchMethodException {
301
302        _chartPanel = new ChartPanel(chart);
303        _chartPanel.setMouseZoomable(true);
304        _chartPanel.setFillZoomRectangle(false);    //  Use an outline (performance issues on Windows with filled rectangle).
305        _chartPanel.setHorizontalAxisTrace(false);
306        _chartPanel.setVerticalAxisTrace(false);
307        _chartPanel.setPreferredSize(new Dimension(FRAME_WIDTH-20, FRAME_HEIGHT-50));
308
309        //  Modify the chart panel's pop-up menu.
310        JPopupMenu chartMenu = _chartPanel.getPopupMenu();
311
312        //  Replace the default "Print..." menu item with our own.
313        chartMenu.remove(5);
314
315        //  Add our own "Print..." item to chart menu.
316        JMenuItem item = new JMenuItem(RB.getString("printItemText"));
317        ActionListener listener = AppUtilities.getActionListenerForMethod(target, "handlePrint");
318        item.addActionListener(listener);
319        item.setActionCommand(RB.getString("printItemText"));
320        item.setEnabled(true);
321        chartMenu.add(item, 5);
322
323        //  Add "Page Setup" to the chart menu.
324        item = new JMenuItem(RB.getString("pageSetupItemText"));
325        listener = AppUtilities.getActionListenerForMethod(target, "handlePageSetup");
326        item.addActionListener(listener);
327        item.setActionCommand(RB.getString("pageSetupItemText"));
328        item.setEnabled(true);
329        chartMenu.add(item, 5);
330
331        //  Replace the default "Save As..." item with our own implementation.
332        chartMenu.remove(3);
333
334        //  Add our own "Save As PNG..." item to chart menu.
335        item = new JMenuItem(RB.getString("saveAsPNGItemText"));
336        listener = AppUtilities.getActionListenerForMethod(this, "handleSaveAsPNG");
337        item.addActionListener(listener);
338        item.setActionCommand(RB.getString("saveAsPNGItemText"));
339        item.setEnabled(true);
340        chartMenu.add(item, 3);
341
342        if (HAS_SVG) {
343            //  Add our own "Save As SVG..." item to chart menu.
344            item = new JMenuItem(RB.getString("saveAsSVGItemText"));
345            listener = AppUtilities.getActionListenerForMethod(this, "handleSaveAsSVG");
346            item.addActionListener(listener);
347            item.setActionCommand(RB.getString("saveAsSVGItemText"));
348            item.setEnabled(true);
349            chartMenu.add(item, 4);
350        }
351    }
352
353    /**
354     * Handle the user choosing "Close" from the File menu. This implementation dispatches
355     * a "Window Closing" event to this window.
356     *
357     * @param event The event that caused this method to be called.
358     */
359    public void handleClose(ActionEvent event) {
360        this.dispatchEvent(new WindowEvent(this, WindowEvent.WINDOW_CLOSING));
361    }
362
363    /**
364     * Handle the user choosing "Save As PNG..." from the File menu. Saves the currently
365     * displayed plot as a PNG format image file.
366     *
367     * @param event The event that caused this method to be called.
368     */
369    public void handleSaveAsPNG(ActionEvent event) {
370        try {
371            //  Go off and actually do the Save As PNG....  Catch exceptions here.
372            doSaveAsPNG();
373
374        } catch (IOException e) {
375            String msg = e.getMessage();
376            if (isNull(msg)) {
377                Throwable cause = e.getCause();
378                if (nonNull(cause))
379                    msg = cause.getMessage();
380            }
381            JOptionPane.showMessageDialog(this, msg, RB.getString("ioErrorTitle"), JOptionPane.ERROR_MESSAGE);
382
383        } catch (Exception ex) {
384            AppUtilities.showException(this, RB.getString("unexpectedTitle"), RB.getString("unexpectedMsg"), ex);
385            Logger.getLogger(AbstractPlotWindow.class.getName()).log(Level.WARNING, "", ex);
386        }
387
388    }
389
390    /**
391     * Handle the user choosing "Save As SVG..." from the File menu. Saves the currently
392     * displayed plot as an SVG format file.
393     *
394     * @param event The event that caused this method to be called.
395     */
396    public void handleSaveAsSVG(ActionEvent event) {
397        try {
398            //  Go off and actually do the Save As SVG....  Catch exceptions here.
399            doSaveAsSVG();
400
401        } catch (IOException e) {
402            String msg = e.getMessage();
403            if (isNull(msg)) {
404                Throwable cause = e.getCause();
405                if (nonNull(cause))
406                    msg = cause.getMessage();
407            }
408            JOptionPane.showMessageDialog(this, msg, RB.getString("ioErrorTitle"), JOptionPane.ERROR_MESSAGE);
409
410        } catch (Exception ex) {
411            AppUtilities.showException(this, RB.getString("unexpectedTitle"), RB.getString("unexpectedMsg"), ex);
412            Logger.getLogger(AbstractPlotWindow.class.getName()).log(Level.WARNING, "", ex);
413        }
414
415    }
416
417    /**
418     * Handle the user choosing "Page Setup..." from the File menu. Displays a Page Setup
419     * dialog allowing the user to change the page settings.
420     *
421     * @param event The event that caused this method to be called.
422     */
423    public void handlePageSetup(ActionEvent event) {
424        PrinterJob job = PrinterJob.getPrinterJob();
425        if (isNull(_pf))
426            _pf = job.defaultPage();
427        _pf = job.pageDialog(_pf);
428    }
429
430    /**
431     * Handle the user choosing "Print" from the File menu. Prints the currently displayed
432     * plot.
433     *
434     * @param event The event that caused this method to be called.
435     */
436    public void handlePrint(ActionEvent event) {
437        PrinterJob job = PrinterJob.getPrinterJob();
438
439        if (isNull(_pf))
440            _pf = job.defaultPage();
441        job.setPrintable(getChartPanel(), _pf);
442
443        if (job.printDialog()) {
444            try {
445
446                job.print();
447
448            } catch (PrinterException e) {
449                JOptionPane.showMessageDialog(this, e);
450            }
451        }
452
453    }
454
455    /**
456     * Handle the user choosing "Save As PNG..." from the File menu. This asks the user
457     * for input and then saves the file. This method throws all exceptions.
458     *
459     * @return true if the user cancels, false if the file was saved.
460     */
461    private boolean doSaveAsPNG() throws IOException {
462
463        //  Build the filename to prompt the user with.
464        String extension = "png";
465        String dir = GeomSSGUI.getInstance().getPreferences().getLastPath();
466        String fileName = _dataName;
467        fileName += "." + extension;
468
469        ExtFilenameFilter fnFilter = new ExtFilenameFilter();
470        fnFilter.addExtension(extension);
471
472        //  Ask the user to select a file for saving.
473        File theFile = AppUtilities.selectFile4Save(this,
474                MessageFormat.format(RB.getString("fileSaveDialog"), extension.toUpperCase()),
475                dir, fileName, fnFilter, extension,
476                RB.getString("fileExists"), RB.getString("warningTitle"));
477
478        if (nonNull(theFile)) {
479            if (theFile.exists() && !theFile.canWrite())
480                throw new IOException(RB.getString("canNotWrite2File"));
481
482            //  Write out the PNG file.
483            ChartPanel plotPanel = getChartPanel();
484            ChartUtilities.saveChartAsPNG(theFile, plotPanel.getChart(),
485                    plotPanel.getWidth(), plotPanel.getHeight());
486
487            return false;
488        }
489
490        return true;
491    }
492
493    /**
494     * Handle the user choosing "Save As SVG..." from the File menu. This asks the user
495     * for input and then saves the file. This method throws all exceptions.
496     *
497     * @return true if the user cancels, false if the file was saved.
498     */
499    private boolean doSaveAsSVG() throws IOException {
500
501        //  Build the filename to prompt the user with.
502        String extension = "svg";
503        String dir = GeomSSGUI.getInstance().getPreferences().getLastPath();
504        String fileName = _dataName;
505        fileName += "." + extension;
506
507        ExtFilenameFilter fnFilter = new ExtFilenameFilter();
508        fnFilter.addExtension(extension);
509
510        //  Ask the user to select a file for saving.
511        File theFile = AppUtilities.selectFile4Save(this,
512                MessageFormat.format(RB.getString("fileSaveDialog"), extension.toUpperCase()),
513                dir, fileName, fnFilter, extension,
514                RB.getString("fileExists"), RB.getString("warningTitle"));
515
516        if (nonNull(theFile)) {
517            if (theFile.exists() && !theFile.canWrite())
518                throw new IOException(RB.getString("canNotWrite2File"));
519
520            //  Write out the SVG file.
521            try (FileOutputStream outputStream = new FileOutputStream(theFile)) {
522                ChartPanel plotPanel = getChartPanel();
523                saveChartAsSVG(plotPanel.getChart(), outputStream, plotPanel.getWidth(), plotPanel.getHeight());
524            }
525
526            return false;
527        }
528
529        return true;
530    }
531
532    /**
533     * Write .svg file based on provided JFreeChart.
534     *
535     * @param aChart       Chart to be written out as SVG.
536     * @param outputStream An OutputStream to a file that will hold SVG XML.
537     * @param aWidth       Width of image.
538     * @param aHeight      Height of image.
539     * @throws java.io.IOException If there is any problem saving the SVG file.
540     */
541    private void saveChartAsSVG(JFreeChart aChart, OutputStream outputStream, 
542            int aWidth, int aHeight) throws IOException {
543
544        // use reflection to get the SVG string
545        String svg = generateSVG(aChart, aWidth, aHeight);
546        if (isNull(svg))
547            throw new IOException(RB.getString("svgErrMsg"));
548
549        PrintWriter output = new PrintWriter(new OutputStreamWriter(outputStream, "UTF-8"));
550        output.println("<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\" \"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\">");
551        output.println(svg);
552        output.flush();
553
554    }
555
556    /**
557     * Generates a string containing a rendering of the chart in SVG format. This feature
558     * is only supported if the JFreeSVG library is included on the classpath.
559     *
560     * @return A string containing an SVG element for the current chart, or
561     *         <code>null</code> if there is a problem with the method invocation by
562     *         reflection.
563     */
564    private static String generateSVG(JFreeChart chart, int width, int height) {
565        Graphics2D g2 = createSVGGraphics2D(width, height);
566        if (isNull(g2)) {
567            throw new IllegalStateException("The JFreeSVG library could not be found.");
568        }
569        // we suppress shadow generation, because SVG is a vector format and
570        // the shadow effect is applied via bitmap effects...
571        g2.setRenderingHint(JFreeChart.KEY_SUPPRESS_SHADOW_GENERATION, true);
572        String svg = null;
573        Rectangle2D drawArea = new Rectangle2D.Double(0, 0, width, height);
574        chart.draw(g2, drawArea);
575        try {
576            Method m = g2.getClass().getMethod("getSVGElement");
577            svg = (String)m.invoke(g2);
578        } catch (NoSuchMethodException | SecurityException | IllegalAccessException | IllegalArgumentException | InvocationTargetException ignore) {
579            // null will be returned
580        }
581
582        return svg;
583    }
584
585    /**
586     * Create and return a Graphics2D that points to an SVG writing library.  If the SVG
587     * library can not be found, null is returned.
588     * 
589     * @param w  The width of the SVG Graphics2D to create in pixels.
590     * @param h  The height of the SVG Graphics2D to create in pixels.
591     * @return A Graphics2D that points to an SVG writing library or null if the SVG
592     * library could not be found.
593     */
594    private static Graphics2D createSVGGraphics2D(int w, int h) {
595        try {
596            Class svgGraphics2d = Class.forName("org.jfree.graphics2d.svg.SVGGraphics2D");
597            Constructor ctor = svgGraphics2d.getConstructor(int.class, int.class);
598            return (Graphics2D)ctor.newInstance(w, h);
599        } catch (ClassNotFoundException | NoSuchMethodException | SecurityException | InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException ex) {
600            return null;
601        }
602    }
603
604    /**
605     * Handle the user choosing "Copy" from the Edit menu. Copies the currently displayed
606     * plot as an image to the clipboard.
607     *
608     * @param event The event that caused this method to be called.
609     */
610    public void handleCopy(ActionEvent event) {
611
612        //  Get a copy of the current plot as an image.
613        ChartPanel plotPanel = getChartPanel();
614        int width = plotPanel.getWidth();
615        int height = plotPanel.getHeight();
616        JFreeChart chart = plotPanel.getChart();
617        Image chartImage = chart.createBufferedImage(width * 2, height * 2, width, height, null);
618
619        //  Place the image in the clipboard.
620        setClipboard(chartImage);
621    }
622
623    /**
624     * Method that places an image on the clipboard.
625     */
626    private static void setClipboard(Image image) {
627        ImageSelection imgSel = new ImageSelection(image);
628        Toolkit.getDefaultToolkit().getSystemClipboard().setContents(imgSel, null);
629    }
630}