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}