001package geomss.ui;
002
003import jahuwaldt.io.ExtFilenameFilter;
004import jahuwaldt.js.datareader.*;
005import jahuwaldt.swing.AppUtilities;
006import jahuwaldt.swing.MDIApplication;
007import java.awt.*;
008import java.awt.event.ActionEvent;
009import java.awt.event.ActionListener;
010import java.io.File;
011import java.io.FileOutputStream;
012import java.io.IOException;
013import java.io.OutputStream;
014import java.text.MessageFormat;
015import java.util.ArrayList;
016import java.util.List;
017import static java.util.Objects.isNull;
018import static java.util.Objects.nonNull;
019import static java.util.Objects.requireNonNull;
020import java.util.logging.Level;
021import java.util.logging.Logger;
022import javax.measure.quantity.Dimensionless;
023import javax.measure.unit.Unit;
024import javax.swing.*;
025import javolution.util.FastTable;
026import org.jfree.chart.ChartFactory;
027import org.jfree.chart.ChartPanel;
028import org.jfree.chart.JFreeChart;
029import org.jfree.chart.axis.NumberAxis;
030import org.jfree.chart.labels.StandardXYToolTipGenerator;
031import org.jfree.chart.plot.PlotOrientation;
032import org.jfree.chart.plot.XYPlot;
033import org.jfree.chart.renderer.xy.XYLineAndShapeRenderer;
034import org.jfree.chart.title.LegendTitle;
035import org.jfree.data.xy.XYSeries;
036import org.jfree.data.xy.XYSeriesCollection;
037import org.jfree.ui.RectangleEdge;
038
039/**
040 * An application window that displays a quick plot of the data contained in a DataSet
041 * made up of DataCase objects containing sets of ArrayParam objects. This allows
042 * "overlay" plots of identical parameters from different "runs" or cases.
043 *
044 * <p> Modified by: Joseph A. Huwaldt </p>
045 *
046 * @author Joseph A. Huwaldt Date: October 12, 2015
047 * @version January 31, 2017
048 */
049public class PlotXYWindow extends AbstractPlotWindow {
050    private static final long serialVersionUID = 1L;
051
052    /**
053     * The panel that contains the plot region of our window. Sub-classes may use this to
054     * access the ChartPanel it contains in it's CENTER and the button panel it contains
055     * in it's SOUTH.
056     */
057    protected JPanel _centerPanel = new JPanel(new BorderLayout());
058
059    // A reference to the all the available data.
060    private DataSet _allData;
061
062    // A reference to the particular arrays we are doing the plot of.
063    private final List<ArrayParam> _plottedData = new ArrayList<>();
064
065    //  The independent parameters for each of the plotted arrays in _plottedData.
066    private final List<ArrayParam> _indepData = new ArrayList<>();
067
068    /**
069     * Creates a new instance of a data plot window using the specified list of data
070     * arrays.
071     *
072     * @param title Title of the window.
073     * @param data  A DataCase containing a single case to be plotted.
074     * @throws IllegalArgumentException if the input data set can not be plotted.
075     */
076    public PlotXYWindow(String title, DataCase data) throws IllegalArgumentException {
077        super(title, title);
078
079        DataSet tmpSet = DataSet.newInstance(title);
080        tmpSet.add(requireNonNull(data));
081        initialize(tmpSet);
082
083    }
084
085    /**
086     * Creates a new instance of a data plot window using the specified list of data
087     * arrays.
088     *
089     * @param title Title of the window.
090     * @param data  DataSet containing the cases to be plotted (all cases must have the
091     *              same set of parameters).
092     * @throws IllegalArgumentException if the input data set can not be plotted.
093     */
094    public PlotXYWindow(String title, DataSet data) throws IllegalArgumentException {
095        super(title, title);
096
097        initialize(requireNonNull(data));
098    }
099
100    /**
101     * Used to initialize this plot window.
102     */
103    private void initialize(DataSet data) throws IllegalArgumentException {
104
105        //  Store the initially plotted parameters from each case.
106        for (DataCase aCase : data) {
107            List<ArrayParam> caseArrays = aCase.getAllArrays();
108            _indepData.add(caseArrays.get(0));
109            _plottedData.add(caseArrays.get(1));
110            FastTable.recycle((FastTable<ArrayParam>)caseArrays);
111        }
112
113        // Set up some class variables.
114        _allData = data;
115
116        //  Layout the contents of this window.
117        try {
118            layoutWindow();
119        } catch (NoSuchMethodException ex) {
120            //  Shouldn't happen after early development.
121            Logger.getLogger(PlotXYWindow.class.getName()).log(Level.WARNING, "", ex);
122        }
123    }
124
125    /**
126     * Method that lays out the contents of this window.
127     */
128    private void layoutWindow() throws NoSuchMethodException {
129
130        // Set up this window's user interface.
131        Container cp = getContentPane();
132        cp.add(_centerPanel);
133
134        //  Create user interface buttons.
135        JButton closeButton = new JButton(RESOURCES.getString("closeItemText"));
136        this.getRootPane().setDefaultButton(closeButton);
137        closeButton.addActionListener(new ActionListener() {
138            @Override
139            public void actionPerformed(ActionEvent evt) {
140                handleClose(null);
141            }
142        });
143        closeButton.setToolTipText(RESOURCES.getString("closeItemToolTip"));
144
145        //  Create a button panel and add all the buttons to it.
146        Box buttonPanel = Box.createHorizontalBox();
147        buttonPanel.setAlignmentX(Box.LEFT_ALIGNMENT);
148        buttonPanel.add(Box.createGlue());
149        buttonPanel.add(closeButton);
150        buttonPanel.add(Box.createHorizontalStrut(20));
151        buttonPanel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
152
153        //  Add the options and buttons to the bottom of the window.
154        _centerPanel.add(buttonPanel, BorderLayout.SOUTH);
155
156        //  Create the plot panel containing the plot itself.
157        ChartPanel plotPanel = createPlotPanel(_plottedData, _indepData, _allData, this);
158        _centerPanel.add(plotPanel, BorderLayout.CENTER);
159
160    }
161
162    /**
163     * Method that will build an "Export" menu containing a list of all the DataReader
164     * objects that are capable of writing to files.
165     *
166     * @param title The title for the menu that is created.
167     * @return A JMenu instance containing a list of all DataReader objects that can write
168     *         to files in various formats. If there are no appropriate readers available,
169     *         null is returned.
170     */
171    @Override
172    protected JMenu buildExportMenu(String title) {
173
174        //  Get the list of data readers that are available.
175        DataReader[] allReaders = DataReaderFactory.getAllReaders();
176        if (isNull(allReaders) || allReaders.length < 1)
177            return null;
178
179        //  Create the menu
180        JMenu menu = new JMenu(title);
181
182        //  Loop over all the readers.
183        boolean noneFound = true;
184        int numReaders = allReaders.length;
185        for (int i = 0; i < numReaders; ++i) {
186            DataReader reader = allReaders[i];
187
188            if (reader.canWriteData()) {
189                //  Found a "writer".
190                noneFound = false;
191
192                //  Create a menu item for it.
193                JMenuItem menuItem = new JMenuItem(reader.toString() + "...");
194                menuItem.addActionListener(new ExportAsMenuListener(reader));
195                menuItem.setActionCommand(reader.toString());
196                menuItem.setEnabled(true);
197
198                //  Add the menu item to the menu.
199                menu.add(menuItem);
200            }
201        }
202
203        if (noneFound)
204            menu = null;
205
206        return menu;
207    }
208
209    /**
210     * Defines an action listener for our Export menu items.
211     */
212    private class ExportAsMenuListener implements ActionListener {
213
214        //  Keep a reference to the reader used by this menu.
215
216        private final DataReader reader;
217
218        public ExportAsMenuListener(DataReader reader) {
219            this.reader = reader;
220        }
221
222        @Override
223        public void actionPerformed(ActionEvent evt) {
224            handleSaveAs();
225        }
226
227        /**
228         * Handles saving the data using this menu item's DataReader instance.
229         */
230        private void handleSaveAs() {
231
232            //  Set up the file name filter.
233            String extension = reader.getExtension();
234            ExtFilenameFilter filter = new ExtFilenameFilter(extension, reader.toString() + " File");
235
236            //  Build the directory and filename to prompt the user with.
237            MDIApplication app = MDIApplication.getInstance();
238            String dir = app.getPreferences().getLastPath();
239            String fileName = getDataName();
240
241            //  Replace any ":" characters with "_".
242            fileName = fileName.replace(':', '_');
243
244            //  Replace any "_ " with " ".
245            fileName = fileName.replaceAll("_ ", " ");
246
247            int idx = fileName.lastIndexOf(".");
248            if (idx > 0)
249                fileName = fileName.substring(0, idx);
250            fileName += "." + extension;
251
252            //  Ask the user to select a file for saving.
253            File chosenFile = AppUtilities.selectFile4Save(PlotXYWindow.this,
254                    MessageFormat.format(RESOURCES.getString("fileSaveDialog"), reader.toString()),
255                    dir, fileName, filter, extension,
256                    RESOURCES.getString("fileExists"), RESOURCES.getString("warningTitle"));
257            if (nonNull(chosenFile)) {
258                if (chosenFile.exists() && !chosenFile.canWrite()) {
259                    JOptionPane.showMessageDialog(PlotXYWindow.this,
260                            RESOURCES.getString("canNotWrite2File"),
261                            RESOURCES.getString("ioErrorTitle"), JOptionPane.ERROR_MESSAGE);
262                    return;
263                }
264
265                //  Export the data.
266                exportData(reader, chosenFile);
267
268            }
269        }
270
271        /**
272         * Method that exports the specified data to the specified file using the
273         * specified writer.
274         *
275         * @param writer  The DataReader to use to write out the data.
276         * @param outFile Reference to the file path where the file should be written.
277         */
278        private void exportData(DataReader writer, File outFile) {
279
280            //  Create a List of DataSet objects as required by the writer.
281            List<DataSet> list = new ArrayList<>();
282            list.add(_allData);
283
284            // Write out the plotted tables using the specified writer.
285            try (OutputStream output = new FileOutputStream(outFile)) {
286
287                writer.write(output, list);
288
289            } catch (IOException | ArrayIndexOutOfBoundsException e) {
290                outFile.delete();
291                JOptionPane.showMessageDialog(PlotXYWindow.this, e.getMessage(), RESOURCES.getString("ioErrorTitle"),
292                        JOptionPane.ERROR_MESSAGE);
293            }
294        }
295
296    }   //  end class ExportAsMenuListener
297
298    /**
299     * Creates a panel that contains the plot of the data.
300     *
301     * @param plottedData List of dependent ArrayParam objects to be plotted.
302     * @param indepData   List of independent ArrayParam objects for each dependent array.
303     * @param allData     The DataSet containing all the available data.
304     * @param target      The object that receives the action events. Must have the
305     *                    following methods defined: "handlePrint(ActionEvent e)", and
306     *                    "handlePageSetup(ActionEvent e)".
307     */
308    private ChartPanel createPlotPanel(List<ArrayParam> depData, List<ArrayParam> indepData, DataSet allData, Object target)
309            throws NoSuchMethodException {
310
311        //  Create the chart.
312        JFreeChart chart = createChart(depData, indepData, allData);
313
314        //  Create a panel to put the chart in.
315        createChartPanel(chart, target);
316
317        //  Return a reference to the newly created chart panel.
318        return getChartPanel();
319    }
320
321    /**
322     * Organizes the input data into series for plotting and creates an XY chart object.
323     */
324    private static JFreeChart createChart(List<ArrayParam> depData, List<ArrayParam> indepData, DataSet allData) {
325
326        //  Build up a run list.
327        XYSeriesCollection runList = new XYSeriesCollection();
328
329        List<String> depNames = new ArrayList<>();
330        int depPos = 0;
331        int numSets = indepData.size(); //  The indepData indicates how many sets of data are plotted.
332        int numArrays = depData.size() / numSets;
333        for (int setNum = 0; setNum < numSets; ++setNum) {
334            ArrayParam indepArray = indepData.get(setNum);
335            CharSequence dataSetName = allData.get(setNum).getName();
336
337            //  Loop over the number of variables to be plotted in each set.
338            for (int i = 0; i < numArrays; ++i) {
339                ArrayParam depArray = depData.get(depPos);
340                String depArrayName = createParamLabel(depArray);
341                if (!depNames.contains(depArrayName)) {
342                    depNames.add(depArrayName);
343                }
344                runList.addSeries(createRun(indepArray, depArray, dataSetName));
345                ++depPos;
346            }
347        }
348
349        //  Range axis label
350        String yAxisLabel;
351        if (depNames.size() < 2)
352            //  There is only a single dependent parameter plotted for, potentially, multiple data sets.
353            yAxisLabel = createParamLabel(depData.get(0));
354
355        else {
356            //  There are, potentially, multiple parameters plotted for a single data set.
357
358            //  Build up a label made up of the names of all the plotted paramters.
359            StringBuilder buffer = new StringBuilder();
360            int size = depNames.size();
361            for (int i = 0; i < size; ++i) {
362                buffer.append(depNames.get(i));
363                if (i != size - 1)
364                    buffer.append(", ");
365            }
366            yAxisLabel = buffer.toString();
367        }
368
369        //  Domain axis label.
370        String xAxisLabel = createParamLabel(indepData.get(0));
371
372        //  Create the chart.
373        JFreeChart chart = ChartFactory.createXYLineChart(
374                null, //  No Title
375                xAxisLabel, //  Domain axis label
376                yAxisLabel, //  Range axis label
377                runList, //  Data to be plotted
378                PlotOrientation.VERTICAL, //  Vertical orientation
379                true, //  Include legend
380                true, //  Include tooltips
381                false);                     //  Do not include URLs
382
383        //  Adjust the legend position and hide it by default.
384        LegendTitle legend = chart.getLegend();
385        legend.setPosition(RectangleEdge.RIGHT);
386        legend.setVisible(false);
387        
388        // Set the background color for the chart...
389        chart.setBackgroundPaint(Color.WHITE);
390
391        //  Get a reference to the plot.
392        XYPlot plot = chart.getXYPlot();
393
394        //  Create a renderer that shows lines, shows plot symbols, and has tool-tip values shown.
395        XYLineAndShapeRenderer renderer = new XYLineAndShapeRenderer(true, true);
396        renderer.setBaseToolTipGenerator(new StandardXYToolTipGenerator());
397
398        //  Make the plot use the new renderer.
399        plot.setRenderer(renderer);
400
401        return chart;
402    }
403
404    /**
405     * Creates a single series or run for plotting from the specified dependent &
406     * independent arrays.
407     */
408    private static XYSeries createRun(ArrayParam indep, ArrayParam dependent, CharSequence runName) {
409
410        XYSeries run = new XYSeries(runName.toString(), false);
411        int length = indep.size();
412        for (int i = 0; i < length; ++i) {
413            double indValue = indep.getValue(i);
414            double depValue = dependent.getValue(i);
415            run.add(indValue, depValue);
416        }
417
418        return run;
419    }
420
421    /**
422     * Method that creates a string representation of a parameter in the following format:
423     * "[refData], name, [units]". If the reference data is null, it is not included. If
424     * the units are Dimensionless, they are not included.
425     */
426    private static String createParamLabel(UnitParameter param) {
427        StringBuilder buffer = new StringBuilder();
428
429        Object refData = param.getUserObject();
430        if (refData != null) {
431            buffer.append(refData);
432            buffer.append(", ");
433        }
434        buffer.append(param.getName());
435        Unit unit = param.getUnit();
436        if (!Dimensionless.UNIT.equals(unit)) {
437            buffer.append(" (");
438            buffer.append(unit);
439            buffer.append(")");
440        }
441
442        return buffer.toString();
443    }
444
445    /**
446     * Return a reference to the data set plotted in this window.
447     * 
448     * @return The data set that is plotted in this window.
449     */
450    public DataSet getDataSet() {
451        return _allData;
452    }
453    
454    /**
455     * Returns the chart contained in this window.
456     *
457     * @return The chart contained in this window.
458     */
459    public JFreeChart getChart() {
460        return getChartPanel().getChart();
461    }
462
463    /**
464     * Return the XYPlot contained in this window.
465     * 
466     * @return The XYPlot contained in this window.
467     */
468    public XYPlot getXYPlot() {
469        return getChart().getXYPlot();
470    }
471    
472    /**
473     * Return the line and shape rendered for the plot in this window.
474     * 
475     * @return The line and shape rendered for the plot in this window.
476     */
477    public XYLineAndShapeRenderer getLineAndShapeRenderer() {
478        XYPlot plot = getXYPlot();
479        XYLineAndShapeRenderer renderer = (XYLineAndShapeRenderer)plot.getRenderer();
480        return renderer;
481    }
482    
483    /**
484     * Return the domain axis for the plot in this window.
485     * 
486     * @return The domain axis for the plot in this window.
487     */
488    public NumberAxis getDomainAxis() {
489        XYPlot plot = getXYPlot();
490        NumberAxis axis = (NumberAxis)plot.getDomainAxis();
491        return axis;
492    }
493    
494    /**
495     * Return the range axis for the plot in this window.
496     * 
497     * @return The range axis for the plot in this window.
498     */
499    public NumberAxis getRangeAxis() {
500        XYPlot plot = getXYPlot();
501        NumberAxis axis = (NumberAxis)plot.getRangeAxis();
502        return axis;
503    }
504    
505    /**
506     * Method that toggles the display of plot symbols/shapes for each data point plotted.
507     *
508     * @param visible Pass true to show plot symbols/shapes and false to hide them.
509     */
510    public void setShapesVisible(boolean visible) {
511        XYLineAndShapeRenderer renderer = getLineAndShapeRenderer();
512        renderer.setBaseShapesVisible(visible);
513    }
514
515    /**
516     * Method that toggles the display of plot symbols/shapes for each data point plotted
517     * in the given series.
518     *
519     * @param series  The series/case/run index (zero-based) to set the shape visibility
520     *                for.
521     * @param visible Pass true to show plot symbols/shapes and false to hide them.
522     */
523    public void setShapesVisible(int series, boolean visible) {
524        XYLineAndShapeRenderer renderer = getLineAndShapeRenderer();
525        renderer.setSeriesShapesVisible(series, visible);
526    }
527
528    /**
529     * Method that toggles the display of lines between the data points being plotted.
530     *
531     * @param visible Pass true to show lines and false to hide them.
532     */
533    public void setLinesVisible(boolean visible) {
534        XYLineAndShapeRenderer renderer = getLineAndShapeRenderer();
535        renderer.setBaseLinesVisible(visible);
536    }
537
538    /**
539     * Method that toggles the display of lines between the data points being plotted in
540     * a given series.
541     *
542     * @param series  The series/case/run index (zero-based) to set the line visibility
543     *                for.
544     * @param visible Pass true to show lines and false to hide them.
545     */
546    public void setLinesVisible(int series, boolean visible) {
547        XYLineAndShapeRenderer renderer = getLineAndShapeRenderer();
548        renderer.setSeriesLinesVisible(series, visible);
549    }
550
551    /**
552     * Set the series Paint for a specified series.
553     * 
554     * @param series  The series/case/run index (zero-based) to set the Paint for.
555     * @param paint   The paint to set for the given series.
556     */
557    public void setSeriesPaint(int series, Paint paint) {
558        XYLineAndShapeRenderer renderer = getLineAndShapeRenderer();
559        renderer.setSeriesPaint(series, requireNonNull(paint));
560    }
561    
562    /**
563     * Set the base stroke for the chart.
564     * 
565     * @param stroke  The stroke to set as the base stroke.
566     */
567    public void setStroke(Stroke stroke) {
568        XYLineAndShapeRenderer renderer = getLineAndShapeRenderer();
569        renderer.setBaseStroke(requireNonNull(stroke));
570    }
571    
572    /**
573     * Set the series stroke for a specified series.
574     * 
575     * @param series  The series/case/run index (zero-based) to set the stroke for.
576     * @param stroke  The stroke to set for the given series.
577     */
578    public void setStroke(int series, Stroke stroke) {
579        XYLineAndShapeRenderer renderer = getLineAndShapeRenderer();
580        renderer.setSeriesStroke(series, requireNonNull(stroke));
581    }
582    
583    /**
584     * Method that toggles the display of the plot legend.
585     * 
586     * @param visible Pass true to show the legend and false to hid it.
587     */
588    public void setLegendVisible(boolean visible) {
589        getChart().getLegend().setVisible(visible);
590    }
591    
592    /**
593     * Sets the chart title. If there is an existing title, its text is updated, otherwise
594     * a new title using the default font is added to the chart.
595     *
596     * @param title The title text (null is permitted and will hide the chart title).
597     */
598    public void setChartTitle(String title) {
599        getChart().setTitle(requireNonNull(title));
600    }
601    
602    /**
603     * Return the domain or X-axis label for this chart.
604     * @return The domain or X-axis label for this chart.
605     */
606    public String getDomainAxisLabel() {
607        NumberAxis axis = getDomainAxis();
608        return axis.getLabel();
609    }
610    
611    /**
612     * Sets the domain or X-axis label.
613     * @param label The new label for the domain axis.
614     */
615    public void setDomainAxisLabel(String label) {
616        NumberAxis axis = getDomainAxis();
617        axis.setLabel(requireNonNull(label));
618    }
619
620    /**
621     * Return the range or Y-axis label for this chart.
622     * @return The range or Y-axis label for this chart.
623     */
624    public String getRangeAxisLabel() {
625        NumberAxis axis = getRangeAxis();
626        return axis.getLabel();
627    }
628    
629    /**
630     * Sets the range or Y-axis label.
631     * @param label The new label for the range axis.
632     */
633    public void setRangeAxisLabel(String label) {
634        NumberAxis axis = getRangeAxis();
635        axis.setLabel(requireNonNull(label));
636    }
637    
638    /**
639     * Sets the domain axis range to the given values and sets the auto-range flag to
640     * false.
641     * 
642     * @param lower the lower axis limit.
643     * @param upper the upper axis limit.
644     */
645    public void setDomainAxisRange(double lower, double upper) {
646        NumberAxis axis = getDomainAxis();
647        axis.setRange(lower, upper);
648    }
649
650    /**
651     * Sets the range axis range to the given values and sets the auto-range flag to
652     * false.
653     * 
654     * @param lower the lower axis limit.
655     * @param upper the upper axis limit.
656     */
657    public void setRangeAxisRange(double lower, double upper) {
658        NumberAxis axis = getRangeAxis();
659        axis.setRange(lower, upper);
660    }
661    
662    /**
663     * Sets the labels used to display all the legend items to the supplied String values.
664     *
665     * @param labels The list of String values to set the legend labels to. This list must
666     *               not be larger than the number of plotted data series.
667     */
668    public void setLegendLabels(String... labels) {
669        int numLabels = labels.length;
670        XYPlot plot = getXYPlot();
671        XYSeriesCollection runList = (XYSeriesCollection)plot.getDataset();
672        if (numLabels > runList.getSeriesCount())
673            throw new IllegalArgumentException(RESOURCES.getString("toManyLabels"));
674        
675        for (int i=0; i < numLabels; ++i) {
676            XYSeries run = runList.getSeries(i);
677            run.setKey(labels[i]);
678        }
679    }
680
681    /**
682     * Sets the labels used to display all the legend items to the supplied String values.
683     *
684     * @param index  The index of the plotted data series to change the label for.
685     * @param label  The new label to apply to the specified data series.
686     */
687    public void setLegendLabel(int index, String label) {
688        XYPlot plot = getXYPlot();
689        XYSeriesCollection runList = (XYSeriesCollection)plot.getDataset();
690        runList.getSeries(index).setKey(requireNonNull(label));
691    }
692}