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