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.Config;
29 import tim.prune.ExternalTools;
30 import tim.prune.GenericFunction;
31 import tim.prune.I18nManager;
32 import tim.prune.data.Altitude;
33 import tim.prune.data.DataPoint;
34 import tim.prune.data.Distance;
35 import tim.prune.data.Field;
36 import tim.prune.data.Timestamp;
37 import tim.prune.data.Track;
38 import tim.prune.data.Distance.Units;
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";
93 _dialog = new JDialog(_parentFrame, I18nManager.getText(getNameKey()), true);
94 _dialog.setLocationRelativeTo(_parentFrame);
95 _dialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE);
96 _dialog.getContentPane().add(makeDialogComponents());
99 if (setupDialog(_app.getTrackInfo().getTrack())) {
100 _dialog.setVisible(true);
103 _app.showErrorMessage(getNameKey(), "dialog.charts.needaltitudeortimes");
109 * Make the dialog components
110 * @return panel containing gui elements
112 private JPanel makeDialogComponents()
114 JPanel dialogPanel = new JPanel();
115 dialogPanel.setLayout(new BorderLayout());
117 JPanel mainPanel = new JPanel();
118 mainPanel.setLayout(new BoxLayout(mainPanel, BoxLayout.Y_AXIS));
120 JPanel axisPanel = new JPanel();
121 axisPanel.setBorder(BorderFactory.createTitledBorder(I18nManager.getText("dialog.charts.xaxis")));
122 _distanceRadio = new JRadioButton(I18nManager.getText("fieldname.distance"));
123 _distanceRadio.setSelected(true);
124 _timeRadio = new JRadioButton(I18nManager.getText("fieldname.time"));
125 ButtonGroup axisGroup = new ButtonGroup();
126 axisGroup.add(_distanceRadio); axisGroup.add(_timeRadio);
127 axisPanel.add(_distanceRadio); axisPanel.add(_timeRadio);
128 mainPanel.add(axisPanel);
131 JPanel yPanel = new JPanel();
132 yPanel.setBorder(BorderFactory.createTitledBorder(I18nManager.getText("dialog.charts.yaxis")));
133 _yAxesBoxes = new JCheckBox[4]; // dist altitude speed vertspeed (time not available on y axis)
134 _yAxesBoxes[0] = new JCheckBox(I18nManager.getText("fieldname.distance"));
135 _yAxesBoxes[1] = new JCheckBox(I18nManager.getText("fieldname.altitude"));
136 _yAxesBoxes[1].setSelected(true);
137 _yAxesBoxes[2] = new JCheckBox(I18nManager.getText("fieldname.speed"));
138 _yAxesBoxes[3] = new JCheckBox(I18nManager.getText("fieldname.verticalspeed"));
139 for (int i=0; i<4; i++) {
140 yPanel.add(_yAxesBoxes[i]);
142 mainPanel.add(yPanel);
144 // Add validation to prevent choosing invalid (ie dist/dist) combinations
145 ActionListener xAxisListener = new ActionListener() {
146 public void actionPerformed(ActionEvent e) {
147 enableYbox(0, _timeRadio.isSelected());
150 _timeRadio.addActionListener(xAxisListener);
151 _distanceRadio.addActionListener(xAxisListener);
154 JPanel outputPanel = new JPanel();
155 outputPanel.setBorder(BorderFactory.createTitledBorder(I18nManager.getText("dialog.charts.output")));
156 outputPanel.setLayout(new BorderLayout());
157 JPanel radiosPanel = new JPanel();
158 JRadioButton screenRadio = new JRadioButton(I18nManager.getText("dialog.charts.screen"));
159 screenRadio.setSelected(true);
160 _svgRadio = new JRadioButton(I18nManager.getText("dialog.charts.svg"));
161 ButtonGroup outputGroup = new ButtonGroup();
162 outputGroup.add(screenRadio); outputGroup.add(_svgRadio);
163 radiosPanel.add(screenRadio); radiosPanel.add(_svgRadio);
164 outputPanel.add(radiosPanel, BorderLayout.NORTH);
165 // panel for svg width, height
166 JPanel sizePanel = new JPanel();
167 sizePanel.setLayout(new GridLayout(2, 2, 10, 1));
168 JLabel widthLabel = new JLabel(I18nManager.getText("dialog.charts.svgwidth"));
169 widthLabel.setHorizontalAlignment(SwingConstants.RIGHT);
170 sizePanel.add(widthLabel);
171 _svgWidthField = new JTextField(DEFAULT_SVG_WIDTH, 5);
172 sizePanel.add(_svgWidthField);
173 JLabel heightLabel = new JLabel(I18nManager.getText("dialog.charts.svgheight"));
174 heightLabel.setHorizontalAlignment(SwingConstants.RIGHT);
175 sizePanel.add(heightLabel);
176 _svgHeightField = new JTextField(DEFAULT_SVG_HEIGHT, 5);
177 sizePanel.add(_svgHeightField);
179 outputPanel.add(sizePanel, BorderLayout.EAST);
180 mainPanel.add(outputPanel);
181 dialogPanel.add(mainPanel, BorderLayout.CENTER);
183 // button panel on bottom
184 JPanel buttonPanel = new JPanel();
185 buttonPanel.setLayout(new FlowLayout(FlowLayout.RIGHT));
187 JButton gnuplotButton = new JButton(I18nManager.getText("button.gnuplotpath"));
188 gnuplotButton.addActionListener(new ActionListener() {
189 public void actionPerformed(ActionEvent e) {
193 buttonPanel.add(gnuplotButton);
195 JButton cancelButton = new JButton(I18nManager.getText("button.cancel"));
196 cancelButton.addActionListener(new ActionListener() {
197 public void actionPerformed(ActionEvent e) {
198 _dialog.setVisible(false);
201 buttonPanel.add(cancelButton);
203 JButton okButton = new JButton(I18nManager.getText("button.ok"));
204 okButton.addActionListener(new ActionListener() {
205 public void actionPerformed(ActionEvent e) {
206 showChart(_app.getTrackInfo().getTrack());
207 _dialog.setVisible(false);
210 buttonPanel.add(okButton);
211 dialogPanel.add(buttonPanel, BorderLayout.SOUTH);
217 * Set up the dialog according to the track contents
218 * @param inTrack track object
219 * @return true if it's all ok
221 private boolean setupDialog(Track inTrack)
223 boolean hasTimes = inTrack.hasData(Field.TIMESTAMP);
224 boolean hasAltitudes = inTrack.getAltitudeRange().hasRange();
225 _timeRadio.setEnabled(hasTimes);
227 // Add checks to prevent choosing unavailable combinations
229 _distanceRadio.setSelected(true);
231 enableYbox(0, !_distanceRadio.isSelected());
232 enableYbox(1, hasAltitudes);
233 enableYbox(2, hasTimes);
234 enableYbox(3, hasTimes && hasAltitudes);
235 return (hasTimes || hasAltitudes);
240 * Enable or disable the given y axis checkbox
241 * @param inIndex index of checkbox
242 * @param inFlag true to enable
244 private void enableYbox(int inIndex, boolean inFlag)
246 _yAxesBoxes[inIndex].setEnabled(inFlag);
248 _yAxesBoxes[inIndex].setSelected(inFlag);
253 * Show the chart for the specified track
254 * @param inTrack track object containing data
256 private void showChart(Track inTrack)
259 for (int i=0; i<_yAxesBoxes.length; i++) {
260 if (_yAxesBoxes[i].isSelected()) {
264 // Select default chart if none selected
265 if (numCharts == 0) {
266 _yAxesBoxes[1].setSelected(true);
269 int[] heights = getHeights(numCharts);
271 boolean showSvg = _svgRadio.isSelected();
274 svgFile = selectSvgFile();
275 if (svgFile == null) {showSvg = false;}
277 OutputStreamWriter writer = null;
280 Process process = Runtime.getRuntime().exec(Config.getGnuplotPath() + " -persist");
281 writer = new OutputStreamWriter(process.getOutputStream());
284 writer.write("set terminal svg size " + getSvgValue(_svgWidthField, DEFAULT_SVG_WIDTH) + " "
285 + getSvgValue(_svgHeightField, DEFAULT_SVG_HEIGHT) + "\n");
286 writer.write("set out '" + svgFile.getAbsolutePath() + "'\n");
289 writer.write("set multiplot layout " + numCharts + ",1\n");
291 // Loop over possible charts
293 for (int c=0; c<_yAxesBoxes.length; c++)
295 if (_yAxesBoxes[c].isSelected())
297 writer.write("set size 1," + (0.01*heights[chartNum*2+1]) + "\n");
298 writer.write("set origin 0," + (0.01*heights[chartNum*2]) + "\n");
299 writeChart(writer, inTrack, _distanceRadio.isSelected(), c);
303 // Close multiplot if open
305 writer.write("unset multiplot\n");
308 catch (Exception e) {
309 _app.showErrorMessageNoLookup(getNameKey(), e.getMessage());
314 if (writer != null) writer.close();
316 catch (Exception e) {} // ignore
322 * Parse the given text field's value and return as string
323 * @param inField text field to read from
324 * @param inDefault default value if not valid
325 * @return value of svg dimension as string
327 private static String getSvgValue(JTextField inField, String inDefault)
331 value = Integer.parseInt(inField.getText());
333 catch (Exception e) {} // ignore, value stays zero
342 * Write out the selected chart to the given Writer object
343 * @param inWriter writer object
344 * @param inTrack Track containing data
345 * @param inDistance true if x axis is distance
346 * @param inYaxis index of y axis
347 * @throws IOException if writing error occurred
349 private static void writeChart(OutputStreamWriter inWriter, Track inTrack, boolean inDistance, int inYaxis)
352 ChartSeries xValues = null, yValues = null;
353 ChartSeries distValues = getDistanceValues(inTrack);
354 // Choose x values according to axis
356 xValues = distValues;
359 xValues = getTimeValues(inTrack);
361 // Choose y values according to axis
364 case 0: // y axis is distance
365 yValues = distValues;
367 case 1: // y axis is altitude
368 yValues = getAltitudeValues(inTrack);
370 case 2: // y axis is speed
371 yValues = getSpeedValues(inTrack);
373 case 3: // y axis is vertical speed
374 yValues = getVertSpeedValues(inTrack);
377 // Make a temporary data file for the output (one per subchart)
378 File tempFile = File.createTempFile("prunedata", null);
379 tempFile.deleteOnExit();
380 // write out values for x and y to temporary file
381 FileWriter tempFileWriter = null;
383 tempFileWriter = new FileWriter(tempFile);
384 tempFileWriter.write("# Temporary data file for Prune charts\n\n");
385 for (int i=0; i<inTrack.getNumPoints(); i++) {
386 if (xValues.hasData(i) && yValues.hasData(i)) {
387 tempFileWriter.write("" + xValues.getData(i) + ", " + yValues.getData(i) + "\n");
391 catch (IOException ioe) { // rethrow
396 tempFileWriter.close();
398 catch (Exception e) {}
403 inWriter.write("set xlabel '" + I18nManager.getText("fieldname.distance") + " (" + getUnitsLabel("units.kilometres.short", "units.miles.short") + ")'\n");
406 inWriter.write("set xlabel '" + I18nManager.getText("fieldname.time") + " (" + I18nManager.getText("units.hours") + ")'\n");
409 // set other labels and plot chart
410 String chartTitle = null;
413 case 0: // y axis is distance
414 inWriter.write("set ylabel '" + I18nManager.getText("fieldname.distance") + " (" + getUnitsLabel("units.kilometres.short", "units.miles.short") + ")'\n");
415 chartTitle = I18nManager.getText("fieldname.distance");
417 case 1: // y axis is altitude
418 inWriter.write("set ylabel '" + I18nManager.getText("fieldname.altitude") + " (" + getUnitsLabel("units.metres.short", "units.feet.short") + ")'\n");
419 chartTitle = I18nManager.getText("fieldname.altitude");
421 case 2: // y axis is speed
422 inWriter.write("set ylabel '" + I18nManager.getText("fieldname.speed") + " (" + getUnitsLabel("units.kmh", "units.mph") + ")'\n");
423 chartTitle = I18nManager.getText("fieldname.speed");
425 case 3: // y axis is vertical speed
426 inWriter.write("set ylabel '" + I18nManager.getText("fieldname.verticalspeed") + " (" + getUnitsLabel("units.metrespersec", "units.feetpersec") + ")'\n");
427 chartTitle = I18nManager.getText("fieldname.verticalspeed");
430 inWriter.write("set style fill solid 0.5 border -1\n");
431 inWriter.write("plot '" + tempFile.getAbsolutePath() + "' title '" + chartTitle + "' with filledcurve y1=0 lt rgb \"#009000\"\n");
435 * Get the units label for the given keys
436 * @param inMetric key if metric
437 * @param inImperial key if imperial
438 * @return display label with appropriate text
440 private static String getUnitsLabel(String inMetric, String inImperial)
442 String key = Config.getUseMetricUnits()?inMetric:inImperial;
443 return I18nManager.getText(key);
448 * Calculate the distance values for each point in the given track
449 * @param inTrack track object
450 * @return distance values in a ChartSeries object
452 private static ChartSeries getDistanceValues(Track inTrack)
454 // Calculate distances and fill in in values array
455 ChartSeries values = new ChartSeries(inTrack.getNumPoints());
456 double totalRads = 0;
457 DataPoint prevPoint = null, currPoint = null;
458 for (int i=0; i<inTrack.getNumPoints(); i++)
460 currPoint = inTrack.getPoint(i);
461 if (prevPoint != null && !currPoint.isWaypoint() && !currPoint.getSegmentStart())
463 totalRads += DataPoint.calculateRadiansBetween(prevPoint, currPoint);
465 if (Config.getUseMetricUnits()) {
466 values.setData(i, Distance.convertRadiansToDistance(totalRads, Units.KILOMETRES));
468 values.setData(i, Distance.convertRadiansToDistance(totalRads, Units.MILES));
470 prevPoint = currPoint;
476 * Calculate the time values for each point in the given track
477 * @param inTrack track object
478 * @return time values in a ChartSeries object
480 private static ChartSeries getTimeValues(Track inTrack)
482 // Calculate times and fill in in values array
483 ChartSeries values = new ChartSeries(inTrack.getNumPoints());
484 double seconds = 0.0;
485 Timestamp prevTimestamp = null;
486 DataPoint currPoint = null;
487 for (int i=0; i<inTrack.getNumPoints(); i++)
489 currPoint = inTrack.getPoint(i);
490 if (currPoint.hasTimestamp())
492 if (!currPoint.getSegmentStart() && prevTimestamp != null) {
493 seconds += (currPoint.getTimestamp().getSecondsSince(prevTimestamp));
495 values.setData(i, seconds / 60.0 / 60.0);
496 prevTimestamp = currPoint.getTimestamp();
503 * Calculate the altitude values for each point in the given track
504 * @param inTrack track object
505 * @return altitude values in a ChartSeries object
507 private static ChartSeries getAltitudeValues(Track inTrack)
509 ChartSeries values = new ChartSeries(inTrack.getNumPoints());
510 Altitude.Format altFormat = Config.getUseMetricUnits()?Altitude.Format.METRES:Altitude.Format.FEET;
511 for (int i=0; i<inTrack.getNumPoints(); i++) {
512 if (inTrack.getPoint(i).hasAltitude()) {
513 values.setData(i, inTrack.getPoint(i).getAltitude().getValue(altFormat));
520 * Calculate the speed values for each point in the given track
521 * @param inTrack track object
522 * @return speed values in a ChartSeries object
524 private static ChartSeries getSpeedValues(Track inTrack)
526 // Calculate speeds and fill in in values array
527 ChartSeries values = new ChartSeries(inTrack.getNumPoints());
528 DataPoint prevPoint = null, currPoint = null, nextPoint = null;
529 DataPoint[] points = getDataPoints(inTrack, false);
530 // Loop over collected points
531 for (int i=1; i<(points.length-1); i++)
533 prevPoint = points[i-1];
534 currPoint = points[i];
535 nextPoint = points[i+1];
536 if (prevPoint != null && currPoint != null && nextPoint != null
537 && nextPoint.getTimestamp().isAfter(currPoint.getTimestamp())
538 && currPoint.getTimestamp().isAfter(prevPoint.getTimestamp()))
540 // Calculate average speed between prevPoint and nextPoint
541 double rads = DataPoint.calculateRadiansBetween(prevPoint, currPoint)
542 + DataPoint.calculateRadiansBetween(currPoint, nextPoint);
543 double time = nextPoint.getTimestamp().getSecondsSince(prevPoint.getTimestamp()) / 60.0 / 60.0;
544 // Convert to distance and pass to chartseries
545 if (Config.getUseMetricUnits()) {
546 values.setData(i, Distance.convertRadiansToDistance(rads, Units.KILOMETRES) / time);
548 values.setData(i, Distance.convertRadiansToDistance(rads, Units.MILES) / time);
556 * Calculate the vertical speed values for each point in the given track
557 * @param inTrack track object
558 * @return vertical speed values in a ChartSeries object
560 private static ChartSeries getVertSpeedValues(Track inTrack)
562 // Calculate speeds and fill in in values array
563 ChartSeries values = new ChartSeries(inTrack.getNumPoints());
564 Altitude.Format altFormat = Config.getUseMetricUnits()?Altitude.Format.METRES:Altitude.Format.FEET;
565 DataPoint prevPoint = null, currPoint = null, nextPoint = null;
566 DataPoint[] points = getDataPoints(inTrack, true); // require that points have altitudes too
567 // Loop over collected points
568 for (int i=1; i<(points.length-1); i++)
570 prevPoint = points[i-1];
571 currPoint = points[i];
572 nextPoint = points[i+1];
573 if (prevPoint != null && currPoint != null && nextPoint != null
574 && nextPoint.getTimestamp().isAfter(currPoint.getTimestamp())
575 && currPoint.getTimestamp().isAfter(prevPoint.getTimestamp()))
577 // Calculate average vertical speed between prevPoint and nextPoint
578 double vspeed = (nextPoint.getAltitude().getValue(altFormat) - prevPoint.getAltitude().getValue(altFormat))
579 * 1.0 / nextPoint.getTimestamp().getSecondsSince(prevPoint.getTimestamp());
580 values.setData(i, vspeed);
588 * Get an array of DataPoints with data for the charts
589 * @param inTrack track object containing points
590 * @param inRequireAltitudes true if only points with altitudes are considered
591 * @return array of points with contiguous non-null elements (<= size) with timestamps
593 private static DataPoint[] getDataPoints(Track inTrack, boolean inRequireAltitudes)
595 DataPoint[] points = new DataPoint[inTrack.getNumPoints()];
596 DataPoint currPoint = null;
598 // Loop over all points
599 for (int i=0; i<inTrack.getNumPoints(); i++)
601 currPoint = inTrack.getPoint(i);
602 if (currPoint != null && !currPoint.isWaypoint() && currPoint.hasTimestamp()
603 && (!inRequireAltitudes || currPoint.hasAltitude()))
605 points[pointNum] = currPoint;
609 // Any elements at the end of the array will stay null
610 // Also note, chronological order is not checked
616 * Select a file to write for the SVG output
619 private File selectSvgFile()
621 if (_fileChooser == null)
623 _fileChooser = new JFileChooser();
624 _fileChooser.setDialogType(JFileChooser.SAVE_DIALOG);
625 _fileChooser.setFileFilter(new GenericFileFilter("filetype.svg", new String[] {"svg"}));
626 _fileChooser.setAcceptAllFileFilterUsed(false);
627 // start from directory in config which should be set
628 File configDir = Config.getWorkingDirectory();
629 if (configDir != null) {_fileChooser.setCurrentDirectory(configDir);}
631 boolean chooseAgain = true;
635 if (_fileChooser.showSaveDialog(_parentFrame) == JFileChooser.APPROVE_OPTION)
637 // OK pressed and file chosen
638 File file = _fileChooser.getSelectedFile();
639 // Check file extension
640 if (!file.getName().toLowerCase().endsWith(".svg")) {
641 file = new File(file.getAbsolutePath() + ".svg");
643 // Check if file exists and if necessary prompt for overwrite
644 Object[] buttonTexts = {I18nManager.getText("button.overwrite"), I18nManager.getText("button.cancel")};
645 if (!file.exists() || (file.canWrite() && JOptionPane.showOptionDialog(_parentFrame,
646 I18nManager.getText("dialog.save.overwrite.text"),
647 I18nManager.getText("dialog.save.overwrite.title"), JOptionPane.YES_NO_OPTION,
648 JOptionPane.WARNING_MESSAGE, null, buttonTexts, buttonTexts[1])
649 == JOptionPane.YES_OPTION))
658 // Cancel pressed so no file selected
664 * @param inNumCharts number of charts to draw
665 * @return array of ints describing position and height of each subchart
667 private static int[] getHeights(int inNumCharts)
669 if (inNumCharts <= 1) {return new int[] {0, 100};}
670 if (inNumCharts == 2) {return new int[] {25, 75, 0, 25};}
671 if (inNumCharts == 3) {return new int[] {40, 60, 20, 20, 0, 20};}
672 return new int[] {54, 46, 36, 18, 18, 18, 0, 18};
676 * Prompt the user to set/edit the path to gnuplot
678 private void setGnuplotPath()
680 String currPath = Config.getGnuplotPath();
681 Object path = JOptionPane.showInputDialog(_dialog,
682 I18nManager.getText("dialog.charts.gnuplotpath"),
683 I18nManager.getText(getNameKey()),
684 JOptionPane.QUESTION_MESSAGE, null, null, "" + currPath);
687 String pathString = path.toString().trim();
688 if (!pathString.equals("") && !pathString.equals(currPath)) {
689 Config.setGnuplotPath(pathString);
690 // warn if gnuplot still not found
691 if (!ExternalTools.isGnuplotInstalled()) {
692 _app.showErrorMessage(getNameKey(), "dialog.charts.gnuplotnotfound");