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.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";
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.getAltitudeRange().hasRange();
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) {}
402 inWriter.write("set xlabel '" + I18nManager.getText("fieldname.distance") + " (" + getUnitsLabel("units.kilometres.short", "units.miles.short") + ")'\n");
405 inWriter.write("set xlabel '" + I18nManager.getText("fieldname.time") + " (" + I18nManager.getText("units.hours") + ")'\n");
408 // set other labels and plot chart
409 String chartTitle = null;
412 case 0: // y axis is distance
413 inWriter.write("set ylabel '" + I18nManager.getText("fieldname.distance") + " (" + getUnitsLabel("units.kilometres.short", "units.miles.short") + ")'\n");
414 chartTitle = I18nManager.getText("fieldname.distance");
416 case 1: // y axis is altitude
417 inWriter.write("set ylabel '" + I18nManager.getText("fieldname.altitude") + " (" + getUnitsLabel("units.metres.short", "units.feet.short") + ")'\n");
418 chartTitle = I18nManager.getText("fieldname.altitude");
420 case 2: // y axis is speed
421 inWriter.write("set ylabel '" + I18nManager.getText("fieldname.speed") + " (" + getUnitsLabel("units.kmh", "units.mph") + ")'\n");
422 chartTitle = I18nManager.getText("fieldname.speed");
424 case 3: // y axis is vertical speed
425 inWriter.write("set ylabel '" + I18nManager.getText("fieldname.verticalspeed") + " (" + getUnitsLabel("units.metrespersec", "units.feetpersec") + ")'\n");
426 chartTitle = I18nManager.getText("fieldname.verticalspeed");
429 inWriter.write("set style fill solid 0.5 border -1\n");
430 inWriter.write("plot '" + tempFile.getAbsolutePath() + "' title '" + chartTitle + "' with filledcurve y1=0 lt rgb \"#009000\"\n");
434 * Get the units label for the given keys
435 * @param inMetric key if metric
436 * @param inImperial key if imperial
437 * @return display label with appropriate text
439 private static String getUnitsLabel(String inMetric, String inImperial)
441 String key = Config.getConfigBoolean(Config.KEY_METRIC_UNITS)?inMetric:inImperial;
442 return I18nManager.getText(key);
447 * Calculate the distance values for each point in the given track
448 * @param inTrack track object
449 * @return distance values in a ChartSeries object
451 private static ChartSeries getDistanceValues(Track inTrack)
453 // Calculate distances and fill in in values array
454 ChartSeries values = new ChartSeries(inTrack.getNumPoints());
455 double totalRads = 0;
456 DataPoint prevPoint = null, currPoint = null;
457 for (int i=0; i<inTrack.getNumPoints(); i++)
459 currPoint = inTrack.getPoint(i);
460 if (prevPoint != null && !currPoint.isWaypoint() && !currPoint.getSegmentStart())
462 totalRads += DataPoint.calculateRadiansBetween(prevPoint, currPoint);
464 if (Config.getConfigBoolean(Config.KEY_METRIC_UNITS)) {
465 values.setData(i, Distance.convertRadiansToDistance(totalRads, Units.KILOMETRES));
467 values.setData(i, Distance.convertRadiansToDistance(totalRads, Units.MILES));
469 prevPoint = currPoint;
475 * Calculate the time values for each point in the given track
476 * @param inTrack track object
477 * @return time values in a ChartSeries object
479 private static ChartSeries getTimeValues(Track inTrack)
481 // Calculate times and fill in in values array
482 ChartSeries values = new ChartSeries(inTrack.getNumPoints());
483 double seconds = 0.0;
484 Timestamp prevTimestamp = null;
485 DataPoint currPoint = null;
486 for (int i=0; i<inTrack.getNumPoints(); i++)
488 currPoint = inTrack.getPoint(i);
489 if (currPoint.hasTimestamp())
491 if (!currPoint.getSegmentStart() && prevTimestamp != null) {
492 seconds += (currPoint.getTimestamp().getSecondsSince(prevTimestamp));
494 values.setData(i, seconds / 60.0 / 60.0);
495 prevTimestamp = currPoint.getTimestamp();
502 * Calculate the altitude values for each point in the given track
503 * @param inTrack track object
504 * @return altitude values in a ChartSeries object
506 private static ChartSeries getAltitudeValues(Track inTrack)
508 ChartSeries values = new ChartSeries(inTrack.getNumPoints());
509 Altitude.Format altFormat = Config.getConfigBoolean(Config.KEY_METRIC_UNITS)?Altitude.Format.METRES:Altitude.Format.FEET;
510 for (int i=0; i<inTrack.getNumPoints(); i++) {
511 if (inTrack.getPoint(i).hasAltitude()) {
512 values.setData(i, inTrack.getPoint(i).getAltitude().getValue(altFormat));
519 * Calculate the speed values for each point in the given track
520 * @param inTrack track object
521 * @return speed values in a ChartSeries object
523 private static ChartSeries getSpeedValues(Track inTrack)
525 // Calculate speeds and fill in in values array
526 ChartSeries values = new ChartSeries(inTrack.getNumPoints());
527 DataPoint prevPoint = null, currPoint = null, nextPoint = null;
528 DataPoint[] points = getDataPoints(inTrack, false);
529 final boolean useMetric = Config.getConfigBoolean(Config.KEY_METRIC_UNITS);
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
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.getConfigBoolean(Config.KEY_METRIC_UNITS)?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
617 * @return selected File object or null if cancelled
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 String configDir = Config.getConfigString(Config.KEY_TRACK_DIR);
629 if (configDir != null) {_fileChooser.setCurrentDirectory(new File(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))
656 // Cancel pressed so no file selected
662 * @param inNumCharts number of charts to draw
663 * @return array of ints describing position and height of each subchart
665 private static int[] getHeights(int inNumCharts)
667 if (inNumCharts <= 1) {return new int[] {0, 100};}
668 if (inNumCharts == 2) {return new int[] {25, 75, 0, 25};}
669 if (inNumCharts == 3) {return new int[] {40, 60, 20, 20, 0, 20};}
670 return new int[] {54, 46, 36, 18, 18, 18, 0, 18};