1 package tim.prune.function.charts;
3 import java.awt.BorderLayout;
4 import java.awt.FlowLayout;
5 import java.awt.GridLayout;
6 import java.awt.event.ActionEvent;
7 import java.awt.event.ActionListener;
9 import java.io.FileWriter;
10 import java.io.IOException;
11 import java.io.OutputStreamWriter;
13 import javax.swing.BorderFactory;
14 import javax.swing.BoxLayout;
15 import javax.swing.ButtonGroup;
16 import javax.swing.JButton;
17 import javax.swing.JCheckBox;
18 import javax.swing.JDialog;
19 import javax.swing.JFileChooser;
20 import javax.swing.JLabel;
21 import javax.swing.JOptionPane;
22 import javax.swing.JPanel;
23 import javax.swing.JRadioButton;
24 import javax.swing.JTextField;
25 import javax.swing.SwingConstants;
28 import tim.prune.ExternalTools;
29 import tim.prune.GenericFunction;
30 import tim.prune.I18nManager;
31 import tim.prune.config.Config;
32 import tim.prune.data.DataPoint;
33 import tim.prune.data.Distance;
34 import tim.prune.data.Field;
35 import tim.prune.data.Timestamp;
36 import tim.prune.data.Track;
37 import tim.prune.gui.profile.SpeedData;
38 import tim.prune.gui.profile.VerticalSpeedData;
39 import tim.prune.load.GenericFileFilter;
42 * Class to manage the generation of charts using gnuplot
44 public class Charter extends GenericFunction
46 /** dialog object, cached */
47 private JDialog _dialog = null;
48 /** radio button for distance axis */
49 private JRadioButton _distanceRadio = null;
50 /** radio button for time axis */
51 private JRadioButton _timeRadio = null;
52 /** array of checkboxes for specifying y axes */
53 private JCheckBox[] _yAxesBoxes = null;
54 /** radio button for svg output */
55 private JRadioButton _svgRadio = null;
56 /** file chooser for saving svg file */
57 private JFileChooser _fileChooser = null;
58 /** text field for svg width */
59 private JTextField _svgWidthField = null;
60 /** text field for svg height */
61 private JTextField _svgHeightField = null;
63 /** Default dimensions of Svg file */
64 private static final String DEFAULT_SVG_WIDTH = "800";
65 private static final String DEFAULT_SVG_HEIGHT = "400";
69 * Constructor from superclass
70 * @param inApp app object
72 public Charter(App inApp)
78 * @return key for function name
80 public String getNameKey()
82 return "function.charts";
90 // First check if gnuplot is available
91 if (!ExternalTools.isToolInstalled(ExternalTools.TOOL_GNUPLOT))
93 _app.showErrorMessage(getNameKey(), "dialog.charts.gnuplotnotfound");
99 _dialog = new JDialog(_parentFrame, I18nManager.getText(getNameKey()), true);
100 _dialog.setLocationRelativeTo(_parentFrame);
101 _dialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE);
102 _dialog.getContentPane().add(makeDialogComponents());
105 if (setupDialog(_app.getTrackInfo().getTrack())) {
106 _dialog.setVisible(true);
109 _app.showErrorMessage(getNameKey(), "dialog.charts.needaltitudeortimes");
115 * Make the dialog components
116 * @return panel containing gui elements
118 private JPanel makeDialogComponents()
120 JPanel dialogPanel = new JPanel();
121 dialogPanel.setLayout(new BorderLayout());
123 JPanel mainPanel = new JPanel();
124 mainPanel.setLayout(new BoxLayout(mainPanel, BoxLayout.Y_AXIS));
126 JPanel axisPanel = new JPanel();
127 axisPanel.setBorder(BorderFactory.createTitledBorder(I18nManager.getText("dialog.charts.xaxis")));
128 _distanceRadio = new JRadioButton(I18nManager.getText("fieldname.distance"));
129 _distanceRadio.setSelected(true);
130 _timeRadio = new JRadioButton(I18nManager.getText("fieldname.time"));
131 ButtonGroup axisGroup = new ButtonGroup();
132 axisGroup.add(_distanceRadio); axisGroup.add(_timeRadio);
133 axisPanel.add(_distanceRadio); axisPanel.add(_timeRadio);
134 mainPanel.add(axisPanel);
137 JPanel yPanel = new JPanel();
138 yPanel.setBorder(BorderFactory.createTitledBorder(I18nManager.getText("dialog.charts.yaxis")));
139 _yAxesBoxes = new JCheckBox[4]; // dist altitude speed vertspeed (time not available on y axis)
140 _yAxesBoxes[0] = new JCheckBox(I18nManager.getText("fieldname.distance"));
141 _yAxesBoxes[1] = new JCheckBox(I18nManager.getText("fieldname.altitude"));
142 _yAxesBoxes[1].setSelected(true);
143 _yAxesBoxes[2] = new JCheckBox(I18nManager.getText("fieldname.speed"));
144 _yAxesBoxes[3] = new JCheckBox(I18nManager.getText("fieldname.verticalspeed"));
145 for (int i=0; i<4; i++) {
146 yPanel.add(_yAxesBoxes[i]);
148 mainPanel.add(yPanel);
150 // Add validation to prevent choosing invalid (ie dist/dist) combinations
151 ActionListener xAxisListener = new ActionListener() {
152 public void actionPerformed(ActionEvent e) {
153 enableYbox(0, _timeRadio.isSelected());
156 _timeRadio.addActionListener(xAxisListener);
157 _distanceRadio.addActionListener(xAxisListener);
160 JPanel outputPanel = new JPanel();
161 outputPanel.setBorder(BorderFactory.createTitledBorder(I18nManager.getText("dialog.charts.output")));
162 outputPanel.setLayout(new BorderLayout());
163 JPanel radiosPanel = new JPanel();
164 JRadioButton screenRadio = new JRadioButton(I18nManager.getText("dialog.charts.screen"));
165 screenRadio.setSelected(true);
166 _svgRadio = new JRadioButton(I18nManager.getText("dialog.charts.svg"));
167 ButtonGroup outputGroup = new ButtonGroup();
168 outputGroup.add(screenRadio); outputGroup.add(_svgRadio);
169 radiosPanel.add(screenRadio); radiosPanel.add(_svgRadio);
170 outputPanel.add(radiosPanel, BorderLayout.NORTH);
171 // panel for svg width, height
172 JPanel sizePanel = new JPanel();
173 sizePanel.setLayout(new GridLayout(2, 2, 10, 1));
174 JLabel widthLabel = new JLabel(I18nManager.getText("dialog.charts.svgwidth"));
175 widthLabel.setHorizontalAlignment(SwingConstants.RIGHT);
176 sizePanel.add(widthLabel);
177 _svgWidthField = new JTextField(DEFAULT_SVG_WIDTH, 5);
178 sizePanel.add(_svgWidthField);
179 JLabel heightLabel = new JLabel(I18nManager.getText("dialog.charts.svgheight"));
180 heightLabel.setHorizontalAlignment(SwingConstants.RIGHT);
181 sizePanel.add(heightLabel);
182 _svgHeightField = new JTextField(DEFAULT_SVG_HEIGHT, 5);
183 sizePanel.add(_svgHeightField);
185 outputPanel.add(sizePanel, BorderLayout.EAST);
186 mainPanel.add(outputPanel);
187 dialogPanel.add(mainPanel, BorderLayout.CENTER);
189 // button panel on bottom
190 JPanel buttonPanel = new JPanel();
191 buttonPanel.setLayout(new FlowLayout(FlowLayout.RIGHT));
193 JButton okButton = new JButton(I18nManager.getText("button.ok"));
194 okButton.addActionListener(new ActionListener() {
195 public void actionPerformed(ActionEvent e) {
196 showChart(_app.getTrackInfo().getTrack());
197 _dialog.setVisible(false);
200 buttonPanel.add(okButton);
202 JButton cancelButton = new JButton(I18nManager.getText("button.cancel"));
203 cancelButton.addActionListener(new ActionListener() {
204 public void actionPerformed(ActionEvent e) {
205 _dialog.setVisible(false);
208 buttonPanel.add(cancelButton);
209 dialogPanel.add(buttonPanel, BorderLayout.SOUTH);
215 * Set up the dialog according to the track contents
216 * @param inTrack track object
217 * @return true if it's all ok
219 private boolean setupDialog(Track inTrack)
221 boolean hasTimes = inTrack.hasData(Field.TIMESTAMP);
222 boolean hasAltitudes = inTrack.hasAltitudeData();
223 _timeRadio.setEnabled(hasTimes);
225 // Add checks to prevent choosing unavailable combinations
227 _distanceRadio.setSelected(true);
229 enableYbox(0, !_distanceRadio.isSelected());
230 enableYbox(1, hasAltitudes);
231 enableYbox(2, hasTimes);
232 enableYbox(3, hasTimes && hasAltitudes);
233 return (hasTimes || hasAltitudes);
238 * Enable or disable the given y axis checkbox
239 * @param inIndex index of checkbox
240 * @param inFlag true to enable
242 private void enableYbox(int inIndex, boolean inFlag)
244 _yAxesBoxes[inIndex].setEnabled(inFlag);
246 _yAxesBoxes[inIndex].setSelected(inFlag);
251 * Show the chart for the specified track
252 * @param inTrack track object containing data
254 private void showChart(Track inTrack)
257 for (int i=0; i<_yAxesBoxes.length; i++) {
258 if (_yAxesBoxes[i].isSelected()) {
262 // Select default chart if none selected
263 if (numCharts == 0) {
264 _yAxesBoxes[1].setSelected(true);
267 int[] heights = getHeights(numCharts);
269 boolean showSvg = _svgRadio.isSelected();
272 svgFile = selectSvgFile();
273 if (svgFile == null) {showSvg = false;}
275 OutputStreamWriter writer = null;
278 final String gnuplotPath = Config.getConfigString(Config.KEY_GNUPLOT_PATH);
279 Process process = Runtime.getRuntime().exec(gnuplotPath + " -persist");
280 writer = new OutputStreamWriter(process.getOutputStream());
283 writer.write("set terminal svg size " + getSvgValue(_svgWidthField, DEFAULT_SVG_WIDTH) + " "
284 + getSvgValue(_svgHeightField, DEFAULT_SVG_HEIGHT) + "\n");
285 writer.write("set out '" + svgFile.getAbsolutePath() + "'\n");
288 writer.write("set multiplot layout " + numCharts + ",1\n");
290 // Loop over possible charts
292 for (int c=0; c<_yAxesBoxes.length; c++)
294 if (_yAxesBoxes[c].isSelected())
296 writer.write("set size 1," + (0.01*heights[chartNum*2+1]) + "\n");
297 writer.write("set origin 0," + (0.01*heights[chartNum*2]) + "\n");
298 writeChart(writer, inTrack, _distanceRadio.isSelected(), c);
302 // Close multiplot if open
304 writer.write("unset multiplot\n");
307 catch (Exception e) {
308 _app.showErrorMessageNoLookup(getNameKey(), e.getMessage());
313 if (writer != null) writer.close();
315 catch (Exception e) {} // ignore
321 * Parse the given text field's value and return as string
322 * @param inField text field to read from
323 * @param inDefault default value if not valid
324 * @return value of svg dimension as string
326 private static String getSvgValue(JTextField inField, String inDefault)
330 value = Integer.parseInt(inField.getText());
332 catch (Exception e) {} // ignore, value stays zero
341 * Write out the selected chart to the given Writer object
342 * @param inWriter writer object
343 * @param inTrack Track containing data
344 * @param inDistance true if x axis is distance
345 * @param inYaxis index of y axis
346 * @throws IOException if writing error occurred
348 private static void writeChart(OutputStreamWriter inWriter, Track inTrack, boolean inDistance, int inYaxis)
351 ChartSeries xValues = null, yValues = null;
352 ChartSeries distValues = getDistanceValues(inTrack);
353 // Choose x values according to axis
355 xValues = distValues;
358 xValues = getTimeValues(inTrack);
360 // Choose y values according to axis
363 case 0: // y axis is distance
364 yValues = distValues;
366 case 1: // y axis is altitude
367 yValues = getAltitudeValues(inTrack);
369 case 2: // y axis is speed
370 yValues = getSpeedValues(inTrack);
372 case 3: // y axis is vertical speed
373 yValues = getVertSpeedValues(inTrack);
376 // Make a temporary data file for the output (one per subchart)
377 File tempFile = File.createTempFile("prunedata", null);
378 tempFile.deleteOnExit();
379 // write out values for x and y to temporary file
380 FileWriter tempFileWriter = null;
382 tempFileWriter = new FileWriter(tempFile);
383 tempFileWriter.write("# Temporary data file for GpsPrune charts\n\n");
384 for (int i=0; i<inTrack.getNumPoints(); i++) {
385 if (xValues.hasData(i) && yValues.hasData(i)) {
386 tempFileWriter.write("" + xValues.getData(i) + ", " + yValues.getData(i) + "\n");
390 catch (IOException ioe) { // rethrow
395 tempFileWriter.close();
397 catch (Exception e) {}
400 // Sort out units to use
401 final String distLabel = I18nManager.getText(Config.getUnitSet().getDistanceUnit().getShortnameKey());
402 final String altLabel = I18nManager.getText(Config.getUnitSet().getAltitudeUnit().getShortnameKey());
403 final String speedLabel = I18nManager.getText(Config.getUnitSet().getSpeedUnit().getShortnameKey());
404 final String vertSpeedLabel = I18nManager.getText(Config.getUnitSet().getVerticalSpeedUnit().getShortnameKey());
408 inWriter.write("set xlabel '" + I18nManager.getText("fieldname.distance") + " (" + distLabel + ")'\n");
411 inWriter.write("set xlabel '" + I18nManager.getText("fieldname.time") + " (" + I18nManager.getText("units.hours") + ")'\n");
414 // set other labels and plot chart
415 String chartTitle = null;
418 case 0: // y axis is distance
419 inWriter.write("set ylabel '" + I18nManager.getText("fieldname.distance") + " (" + distLabel + ")'\n");
420 chartTitle = I18nManager.getText("fieldname.distance");
422 case 1: // y axis is altitude
423 inWriter.write("set ylabel '" + I18nManager.getText("fieldname.altitude") + " (" + altLabel + ")'\n");
424 chartTitle = I18nManager.getText("fieldname.altitude");
426 case 2: // y axis is speed
427 inWriter.write("set ylabel '" + I18nManager.getText("fieldname.speed") + " (" + speedLabel + ")'\n");
428 chartTitle = I18nManager.getText("fieldname.speed");
430 case 3: // y axis is vertical speed
431 inWriter.write("set ylabel '" + I18nManager.getText("fieldname.verticalspeed") + " (" + vertSpeedLabel + ")'\n");
432 chartTitle = I18nManager.getText("fieldname.verticalspeed");
435 inWriter.write("set style fill solid 0.5 border -1\n");
436 inWriter.write("plot '" + tempFile.getAbsolutePath() + "' title '" + chartTitle + "' with filledcurve y1=0 lt rgb \"#009000\"\n");
441 * Calculate the distance values for each point in the given track
442 * @param inTrack track object
443 * @return distance values in a ChartSeries object
445 private static ChartSeries getDistanceValues(Track inTrack)
447 // Calculate distances and fill in in values array
448 ChartSeries values = new ChartSeries(inTrack.getNumPoints());
449 double totalRads = 0;
450 DataPoint prevPoint = null, currPoint = null;
451 for (int i=0; i<inTrack.getNumPoints(); i++)
453 currPoint = inTrack.getPoint(i);
454 if (prevPoint != null && !currPoint.isWaypoint() && !currPoint.getSegmentStart())
456 totalRads += DataPoint.calculateRadiansBetween(prevPoint, currPoint);
459 // distance values use currently configured units
460 values.setData(i, Distance.convertRadiansToDistance(totalRads));
462 prevPoint = currPoint;
468 * Calculate the time values for each point in the given track
469 * @param inTrack track object
470 * @return time values in a ChartSeries object
472 private static ChartSeries getTimeValues(Track inTrack)
474 // Calculate times and fill in in values array
475 ChartSeries values = new ChartSeries(inTrack.getNumPoints());
476 double seconds = 0.0;
477 Timestamp prevTimestamp = null;
478 DataPoint currPoint = null;
479 for (int i=0; i<inTrack.getNumPoints(); i++)
481 currPoint = inTrack.getPoint(i);
482 if (currPoint.hasTimestamp())
484 if (!currPoint.getSegmentStart() && prevTimestamp != null) {
485 seconds += (currPoint.getTimestamp().getSecondsSince(prevTimestamp));
487 values.setData(i, seconds / 60.0 / 60.0);
488 prevTimestamp = currPoint.getTimestamp();
495 * Calculate the altitude values for each point in the given track
496 * @param inTrack track object
497 * @return altitude values in a ChartSeries object
499 private static ChartSeries getAltitudeValues(Track inTrack)
501 ChartSeries values = new ChartSeries(inTrack.getNumPoints());
502 final double multFactor = Config.getUnitSet().getAltitudeUnit().getMultFactorFromStd();
503 for (int i=0; i<inTrack.getNumPoints(); i++) {
504 if (inTrack.getPoint(i).hasAltitude()) {
505 values.setData(i, inTrack.getPoint(i).getAltitude().getMetricValue() * multFactor);
512 * Calculate the speed values for each point in the given track
513 * @param inTrack track object
514 * @return speed values in a ChartSeries object
516 private static ChartSeries getSpeedValues(Track inTrack)
518 // Calculate speeds using the same formula as the profile chart
519 SpeedData speeds = new SpeedData(inTrack);
521 final int numPoints = inTrack.getNumPoints();
522 ChartSeries values = new ChartSeries(numPoints);
523 // Loop over collected points
524 for (int i=0; i<numPoints; i++)
526 if (speeds.hasData(i))
528 values.setData(i, speeds.getData(i));
535 * Calculate the vertical speed values for each point in the given track
536 * @param inTrack track object
537 * @return vertical speed values in a ChartSeries object
539 private static ChartSeries getVertSpeedValues(Track inTrack)
541 // Calculate speeds using the same formula as the profile chart
542 VerticalSpeedData speeds = new VerticalSpeedData(inTrack);
544 final int numPoints = inTrack.getNumPoints();
545 ChartSeries values = new ChartSeries(numPoints);
546 // Loop over collected points
547 for (int i=0; i<numPoints; i++)
549 if (speeds.hasData(i))
551 values.setData(i, speeds.getData(i));
559 * Select a file to write for the SVG output
560 * @return selected File object or null if cancelled
562 private File selectSvgFile()
564 if (_fileChooser == null)
566 _fileChooser = new JFileChooser();
567 _fileChooser.setDialogType(JFileChooser.SAVE_DIALOG);
568 _fileChooser.setFileFilter(new GenericFileFilter("filetype.svg", new String[] {"svg"}));
569 _fileChooser.setAcceptAllFileFilterUsed(false);
570 // start from directory in config which should be set
571 String configDir = Config.getConfigString(Config.KEY_TRACK_DIR);
572 if (configDir != null) {_fileChooser.setCurrentDirectory(new File(configDir));}
574 boolean chooseAgain = true;
578 if (_fileChooser.showSaveDialog(_parentFrame) == JFileChooser.APPROVE_OPTION)
580 // OK pressed and file chosen
581 File file = _fileChooser.getSelectedFile();
582 // Check file extension
583 if (!file.getName().toLowerCase().endsWith(".svg")) {
584 file = new File(file.getAbsolutePath() + ".svg");
586 // Check if file exists and if necessary prompt for overwrite
587 Object[] buttonTexts = {I18nManager.getText("button.overwrite"), I18nManager.getText("button.cancel")};
588 if (!file.exists() || (file.canWrite() && JOptionPane.showOptionDialog(_parentFrame,
589 I18nManager.getText("dialog.save.overwrite.text"),
590 I18nManager.getText("dialog.save.overwrite.title"), JOptionPane.YES_NO_OPTION,
591 JOptionPane.WARNING_MESSAGE, null, buttonTexts, buttonTexts[1])
592 == JOptionPane.YES_OPTION))
599 // Cancel pressed so no file selected
605 * @param inNumCharts number of charts to draw
606 * @return array of ints describing position and height of each subchart
608 private static int[] getHeights(int inNumCharts)
610 if (inNumCharts <= 1) {return new int[] {0, 100};}
611 if (inNumCharts == 2) {return new int[] {25, 75, 0, 25};}
612 if (inNumCharts == 3) {return new int[] {40, 60, 20, 20, 0, 20};}
613 return new int[] {54, 46, 36, 18, 18, 18, 0, 18};