package tim.prune.function.charts; import java.awt.BorderLayout; import java.awt.FlowLayout; import java.awt.GridLayout; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.io.File; import java.io.FileWriter; import java.io.IOException; import java.io.OutputStreamWriter; import javax.swing.BorderFactory; import javax.swing.BoxLayout; import javax.swing.ButtonGroup; import javax.swing.JButton; import javax.swing.JCheckBox; import javax.swing.JDialog; import javax.swing.JFileChooser; import javax.swing.JLabel; import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JRadioButton; import javax.swing.JTextField; import javax.swing.SwingConstants; import tim.prune.App; import tim.prune.ExternalTools; import tim.prune.GenericFunction; import tim.prune.I18nManager; import tim.prune.config.Config; import tim.prune.data.DataPoint; import tim.prune.data.Distance; import tim.prune.data.Field; import tim.prune.data.Timestamp; import tim.prune.data.Track; import tim.prune.gui.profile.SpeedData; import tim.prune.gui.profile.VerticalSpeedData; import tim.prune.load.GenericFileFilter; /** * Class to manage the generation of charts using gnuplot */ public class Charter extends GenericFunction { /** dialog object, cached */ private JDialog _dialog = null; /** radio button for distance axis */ private JRadioButton _distanceRadio = null; /** radio button for time axis */ private JRadioButton _timeRadio = null; /** array of checkboxes for specifying y axes */ private JCheckBox[] _yAxesBoxes = null; /** radio button for svg output */ private JRadioButton _svgRadio = null; /** file chooser for saving svg file */ private JFileChooser _fileChooser = null; /** text field for svg width */ private JTextField _svgWidthField = null; /** text field for svg height */ private JTextField _svgHeightField = null; /** Default dimensions of Svg file */ private static final String DEFAULT_SVG_WIDTH = "800"; private static final String DEFAULT_SVG_HEIGHT = "400"; /** * Constructor from superclass * @param inApp app object */ public Charter(App inApp) { super(inApp); } /** * @return key for function name */ public String getNameKey() { return "function.charts"; } /** * Show the dialog */ public void begin() { // First check if gnuplot is available if (!ExternalTools.isToolInstalled(ExternalTools.TOOL_GNUPLOT)) { _app.showErrorMessage(getNameKey(), "dialog.charts.gnuplotnotfound"); return; } // Make dialog window if (_dialog == null) { _dialog = new JDialog(_parentFrame, I18nManager.getText(getNameKey()), true); _dialog.setLocationRelativeTo(_parentFrame); _dialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE); _dialog.getContentPane().add(makeDialogComponents()); _dialog.pack(); } if (setupDialog(_app.getTrackInfo().getTrack())) { _dialog.setVisible(true); } else { _app.showErrorMessage(getNameKey(), "dialog.charts.needaltitudeortimes"); } } /** * Make the dialog components * @return panel containing gui elements */ private JPanel makeDialogComponents() { JPanel dialogPanel = new JPanel(); dialogPanel.setLayout(new BorderLayout()); JPanel mainPanel = new JPanel(); mainPanel.setLayout(new BoxLayout(mainPanel, BoxLayout.Y_AXIS)); // x axis choice JPanel axisPanel = new JPanel(); axisPanel.setBorder(BorderFactory.createTitledBorder(I18nManager.getText("dialog.charts.xaxis"))); _distanceRadio = new JRadioButton(I18nManager.getText("fieldname.distance")); _distanceRadio.setSelected(true); _timeRadio = new JRadioButton(I18nManager.getText("fieldname.time")); ButtonGroup axisGroup = new ButtonGroup(); axisGroup.add(_distanceRadio); axisGroup.add(_timeRadio); axisPanel.add(_distanceRadio); axisPanel.add(_timeRadio); mainPanel.add(axisPanel); // y axis choices JPanel yPanel = new JPanel(); yPanel.setBorder(BorderFactory.createTitledBorder(I18nManager.getText("dialog.charts.yaxis"))); _yAxesBoxes = new JCheckBox[4]; // dist altitude speed vertspeed (time not available on y axis) _yAxesBoxes[0] = new JCheckBox(I18nManager.getText("fieldname.distance")); _yAxesBoxes[1] = new JCheckBox(I18nManager.getText("fieldname.altitude")); _yAxesBoxes[1].setSelected(true); _yAxesBoxes[2] = new JCheckBox(I18nManager.getText("fieldname.speed")); _yAxesBoxes[3] = new JCheckBox(I18nManager.getText("fieldname.verticalspeed")); for (int i=0; i<4; i++) { yPanel.add(_yAxesBoxes[i]); } mainPanel.add(yPanel); // Add validation to prevent choosing invalid (ie dist/dist) combinations ActionListener xAxisListener = new ActionListener() { public void actionPerformed(ActionEvent e) { enableYbox(0, _timeRadio.isSelected()); } }; _timeRadio.addActionListener(xAxisListener); _distanceRadio.addActionListener(xAxisListener); // output buttons JPanel outputPanel = new JPanel(); outputPanel.setBorder(BorderFactory.createTitledBorder(I18nManager.getText("dialog.charts.output"))); outputPanel.setLayout(new BorderLayout()); JPanel radiosPanel = new JPanel(); JRadioButton screenRadio = new JRadioButton(I18nManager.getText("dialog.charts.screen")); screenRadio.setSelected(true); _svgRadio = new JRadioButton(I18nManager.getText("dialog.charts.svg")); ButtonGroup outputGroup = new ButtonGroup(); outputGroup.add(screenRadio); outputGroup.add(_svgRadio); radiosPanel.add(screenRadio); radiosPanel.add(_svgRadio); outputPanel.add(radiosPanel, BorderLayout.NORTH); // panel for svg width, height JPanel sizePanel = new JPanel(); sizePanel.setLayout(new GridLayout(2, 2, 10, 1)); JLabel widthLabel = new JLabel(I18nManager.getText("dialog.charts.svgwidth")); widthLabel.setHorizontalAlignment(SwingConstants.RIGHT); sizePanel.add(widthLabel); _svgWidthField = new JTextField(DEFAULT_SVG_WIDTH, 5); sizePanel.add(_svgWidthField); JLabel heightLabel = new JLabel(I18nManager.getText("dialog.charts.svgheight")); heightLabel.setHorizontalAlignment(SwingConstants.RIGHT); sizePanel.add(heightLabel); _svgHeightField = new JTextField(DEFAULT_SVG_HEIGHT, 5); sizePanel.add(_svgHeightField); outputPanel.add(sizePanel, BorderLayout.EAST); mainPanel.add(outputPanel); dialogPanel.add(mainPanel, BorderLayout.CENTER); // button panel on bottom JPanel buttonPanel = new JPanel(); buttonPanel.setLayout(new FlowLayout(FlowLayout.RIGHT)); // ok button JButton okButton = new JButton(I18nManager.getText("button.ok")); okButton.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { showChart(_app.getTrackInfo().getTrack()); _dialog.setVisible(false); } }); buttonPanel.add(okButton); // Cancel button JButton cancelButton = new JButton(I18nManager.getText("button.cancel")); cancelButton.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { _dialog.setVisible(false); } }); buttonPanel.add(cancelButton); dialogPanel.add(buttonPanel, BorderLayout.SOUTH); return dialogPanel; } /** * Set up the dialog according to the track contents * @param inTrack track object * @return true if it's all ok */ private boolean setupDialog(Track inTrack) { boolean hasTimes = inTrack.hasData(Field.TIMESTAMP); boolean hasAltitudes = inTrack.hasAltitudeData(); _timeRadio.setEnabled(hasTimes); // Add checks to prevent choosing unavailable combinations if (!hasTimes) { _distanceRadio.setSelected(true); } enableYbox(0, !_distanceRadio.isSelected()); enableYbox(1, hasAltitudes); enableYbox(2, hasTimes); enableYbox(3, hasTimes && hasAltitudes); return (hasTimes || hasAltitudes); } /** * Enable or disable the given y axis checkbox * @param inIndex index of checkbox * @param inFlag true to enable */ private void enableYbox(int inIndex, boolean inFlag) { _yAxesBoxes[inIndex].setEnabled(inFlag); if (!inFlag) { _yAxesBoxes[inIndex].setSelected(inFlag); } } /** * Show the chart for the specified track * @param inTrack track object containing data */ private void showChart(Track inTrack) { int numCharts = 0; for (int i=0; i<_yAxesBoxes.length; i++) { if (_yAxesBoxes[i].isSelected()) { numCharts++; } } // Select default chart if none selected if (numCharts == 0) { _yAxesBoxes[1].setSelected(true); numCharts = 1; } int[] heights = getHeights(numCharts); boolean showSvg = _svgRadio.isSelected(); File svgFile = null; if (showSvg) { svgFile = selectSvgFile(); if (svgFile == null) {showSvg = false;} } OutputStreamWriter writer = null; try { final String gnuplotPath = Config.getConfigString(Config.KEY_GNUPLOT_PATH); Process process = Runtime.getRuntime().exec(gnuplotPath + " -persist"); writer = new OutputStreamWriter(process.getOutputStream()); if (showSvg) { writer.write("set terminal svg size " + getSvgValue(_svgWidthField, DEFAULT_SVG_WIDTH) + " " + getSvgValue(_svgHeightField, DEFAULT_SVG_HEIGHT) + "\n"); writer.write("set out '" + svgFile.getAbsolutePath() + "'\n"); } else { // For screen output, gnuplot should use the default terminal (windows or x11 or wxt or something) } if (numCharts > 1) { writer.write("set multiplot layout " + numCharts + ",1\n"); } // Loop over possible charts int chartNum = 0; for (int c=0; c<_yAxesBoxes.length; c++) { if (_yAxesBoxes[c].isSelected()) { writer.write("set size 1," + (0.01*heights[chartNum*2+1]) + "\n"); writer.write("set origin 0," + (0.01*heights[chartNum*2]) + "\n"); writeChart(writer, inTrack, _distanceRadio.isSelected(), c); chartNum++; } } // Close multiplot if open if (numCharts > 1) { writer.write("unset multiplot\n"); } } catch (Exception e) { _app.showErrorMessageNoLookup(getNameKey(), e.getMessage()); } finally { try { // Close writer if (writer != null) writer.close(); } catch (Exception e) {} // ignore } } /** * Parse the given text field's value and return as string * @param inField text field to read from * @param inDefault default value if not valid * @return value of svg dimension as string */ private static String getSvgValue(JTextField inField, String inDefault) { int value = 0; try { value = Integer.parseInt(inField.getText()); } catch (Exception e) {} // ignore, value stays zero if (value > 0) { return "" + value; } return inDefault; } /** * Write out the selected chart to the given Writer object * @param inWriter writer object * @param inTrack Track containing data * @param inDistance true if x axis is distance * @param inYaxis index of y axis * @throws IOException if writing error occurred */ private static void writeChart(OutputStreamWriter inWriter, Track inTrack, boolean inDistance, int inYaxis) throws IOException { ChartSeries xValues = null, yValues = null; ChartSeries distValues = getDistanceValues(inTrack); // Choose x values according to axis if (inDistance) { xValues = distValues; } else { xValues = getTimeValues(inTrack); } // Choose y values according to axis switch (inYaxis) { case 0: // y axis is distance yValues = distValues; break; case 1: // y axis is altitude yValues = getAltitudeValues(inTrack); break; case 2: // y axis is speed yValues = getSpeedValues(inTrack); break; case 3: // y axis is vertical speed yValues = getVertSpeedValues(inTrack); break; } // Make a temporary data file for the output (one per subchart) File tempFile = File.createTempFile("gpsprunedata", null); tempFile.deleteOnExit(); // write out values for x and y to temporary file FileWriter tempFileWriter = null; try { tempFileWriter = new FileWriter(tempFile); tempFileWriter.write("# Temporary data file for GpsPrune charts\n\n"); for (int i=0; i