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 // For screen output, gnuplot should use the default terminal (windows or x11 or wxt or something)
291 writer.write("set multiplot layout " + numCharts + ",1\n");
293 // Loop over possible charts
295 for (int c=0; c<_yAxesBoxes.length; c++)
297 if (_yAxesBoxes[c].isSelected())
299 writer.write("set size 1," + (0.01*heights[chartNum*2+1]) + "\n");
300 writer.write("set origin 0," + (0.01*heights[chartNum*2]) + "\n");
301 writeChart(writer, inTrack, _distanceRadio.isSelected(), c);
305 // Close multiplot if open
307 writer.write("unset multiplot\n");
310 catch (Exception e) {
311 _app.showErrorMessageNoLookup(getNameKey(), e.getMessage());
316 if (writer != null) writer.close();
318 catch (Exception e) {} // ignore
324 * Parse the given text field's value and return as string
325 * @param inField text field to read from
326 * @param inDefault default value if not valid
327 * @return value of svg dimension as string
329 private static String getSvgValue(JTextField inField, String inDefault)
333 value = Integer.parseInt(inField.getText());
335 catch (Exception e) {} // ignore, value stays zero
344 * Write out the selected chart to the given Writer object
345 * @param inWriter writer object
346 * @param inTrack Track containing data
347 * @param inDistance true if x axis is distance
348 * @param inYaxis index of y axis
349 * @throws IOException if writing error occurred
351 private static void writeChart(OutputStreamWriter inWriter, Track inTrack, boolean inDistance, int inYaxis)
354 ChartSeries xValues = null, yValues = null;
355 ChartSeries distValues = getDistanceValues(inTrack);
356 // Choose x values according to axis
358 xValues = distValues;
361 xValues = getTimeValues(inTrack);
363 // Choose y values according to axis
366 case 0: // y axis is distance
367 yValues = distValues;
369 case 1: // y axis is altitude
370 yValues = getAltitudeValues(inTrack);
372 case 2: // y axis is speed
373 yValues = getSpeedValues(inTrack);
375 case 3: // y axis is vertical speed
376 yValues = getVertSpeedValues(inTrack);
379 // Make a temporary data file for the output (one per subchart)
380 File tempFile = File.createTempFile("gpsprunedata", null);
381 tempFile.deleteOnExit();
382 // write out values for x and y to temporary file
383 FileWriter tempFileWriter = null;
385 tempFileWriter = new FileWriter(tempFile);
386 tempFileWriter.write("# Temporary data file for GpsPrune charts\n\n");
387 for (int i=0; i<inTrack.getNumPoints(); i++) {
388 if (xValues.hasData(i) && yValues.hasData(i)) {
389 tempFileWriter.write("" + xValues.getData(i) + ", " + yValues.getData(i) + "\n");
393 catch (IOException ioe) { // rethrow
398 tempFileWriter.close();
400 catch (Exception e) {}
403 // Sort out units to use
404 final String distLabel = I18nManager.getText(Config.getUnitSet().getDistanceUnit().getShortnameKey());
405 final String altLabel = I18nManager.getText(Config.getUnitSet().getAltitudeUnit().getShortnameKey());
406 final String speedLabel = I18nManager.getText(Config.getUnitSet().getSpeedUnit().getShortnameKey());
407 final String vertSpeedLabel = I18nManager.getText(Config.getUnitSet().getVerticalSpeedUnit().getShortnameKey());
411 inWriter.write("set xlabel '" + I18nManager.getText("fieldname.distance") + " (" + distLabel + ")'\n");
414 inWriter.write("set xlabel '" + I18nManager.getText("fieldname.time") + " (" + I18nManager.getText("units.hours") + ")'\n");
417 // set other labels and plot chart
418 String chartTitle = null;
421 case 0: // y axis is distance
422 inWriter.write("set ylabel '" + I18nManager.getText("fieldname.distance") + " (" + distLabel + ")'\n");
423 chartTitle = I18nManager.getText("fieldname.distance");
425 case 1: // y axis is altitude
426 inWriter.write("set ylabel '" + I18nManager.getText("fieldname.altitude") + " (" + altLabel + ")'\n");
427 chartTitle = I18nManager.getText("fieldname.altitude");
429 case 2: // y axis is speed
430 inWriter.write("set ylabel '" + I18nManager.getText("fieldname.speed") + " (" + speedLabel + ")'\n");
431 chartTitle = I18nManager.getText("fieldname.speed");
433 case 3: // y axis is vertical speed
434 inWriter.write("set ylabel '" + I18nManager.getText("fieldname.verticalspeed") + " (" + vertSpeedLabel + ")'\n");
435 chartTitle = I18nManager.getText("fieldname.verticalspeed");
438 inWriter.write("set style fill solid 0.5 border -1\n");
439 inWriter.write("plot '" + tempFile.getAbsolutePath() + "' title '" + chartTitle + "' with filledcurve y1=0 lt rgb \"#009000\"\n");
444 * Calculate the distance values for each point in the given track
445 * @param inTrack track object
446 * @return distance values in a ChartSeries object
448 private static ChartSeries getDistanceValues(Track inTrack)
450 // Calculate distances and fill in in values array
451 ChartSeries values = new ChartSeries(inTrack.getNumPoints());
452 double totalRads = 0;
453 DataPoint prevPoint = null, currPoint = null;
454 for (int i=0; i<inTrack.getNumPoints(); i++)
456 currPoint = inTrack.getPoint(i);
457 if (prevPoint != null && !currPoint.isWaypoint() && !currPoint.getSegmentStart())
459 totalRads += DataPoint.calculateRadiansBetween(prevPoint, currPoint);
462 // distance values use currently configured units
463 values.setData(i, Distance.convertRadiansToDistance(totalRads));
465 prevPoint = currPoint;
471 * Calculate the time values for each point in the given track
472 * @param inTrack track object
473 * @return time values in a ChartSeries object
475 private static ChartSeries getTimeValues(Track inTrack)
477 // Calculate times and fill in in values array
478 ChartSeries values = new ChartSeries(inTrack.getNumPoints());
479 double seconds = 0.0;
480 Timestamp prevTimestamp = null;
481 DataPoint currPoint = null;
482 for (int i=0; i<inTrack.getNumPoints(); i++)
484 currPoint = inTrack.getPoint(i);
485 if (currPoint.hasTimestamp())
487 if (!currPoint.getSegmentStart() && prevTimestamp != null) {
488 seconds += (currPoint.getTimestamp().getSecondsSince(prevTimestamp));
490 values.setData(i, seconds / 60.0 / 60.0);
491 prevTimestamp = currPoint.getTimestamp();
498 * Calculate the altitude values for each point in the given track
499 * @param inTrack track object
500 * @return altitude values in a ChartSeries object
502 private static ChartSeries getAltitudeValues(Track inTrack)
504 ChartSeries values = new ChartSeries(inTrack.getNumPoints());
505 final double multFactor = Config.getUnitSet().getAltitudeUnit().getMultFactorFromStd();
506 for (int i=0; i<inTrack.getNumPoints(); i++) {
507 if (inTrack.getPoint(i).hasAltitude()) {
508 values.setData(i, inTrack.getPoint(i).getAltitude().getMetricValue() * multFactor);
515 * Calculate the speed values for each point in the given track
516 * @param inTrack track object
517 * @return speed values in a ChartSeries object
519 private static ChartSeries getSpeedValues(Track inTrack)
521 // Calculate speeds using the same formula as the profile chart
522 SpeedData speeds = new SpeedData(inTrack);
523 speeds.init(Config.getUnitSet());
525 final int numPoints = inTrack.getNumPoints();
526 ChartSeries values = new ChartSeries(numPoints);
527 // Loop over collected points
528 for (int i=0; i<numPoints; i++)
530 if (speeds.hasData(i))
532 values.setData(i, speeds.getData(i));
539 * Calculate the vertical speed values for each point in the given track
540 * @param inTrack track object
541 * @return vertical speed values in a ChartSeries object
543 private static ChartSeries getVertSpeedValues(Track inTrack)
545 // Calculate speeds using the same formula as the profile chart
546 VerticalSpeedData speeds = new VerticalSpeedData(inTrack);
547 speeds.init(Config.getUnitSet());
549 final int numPoints = inTrack.getNumPoints();
550 ChartSeries values = new ChartSeries(numPoints);
551 // Loop over collected points
552 for (int i=0; i<numPoints; i++)
554 if (speeds.hasData(i))
556 values.setData(i, speeds.getData(i));
564 * Select a file to write for the SVG output
565 * @return selected File object or null if cancelled
567 private File selectSvgFile()
569 if (_fileChooser == null)
571 _fileChooser = new JFileChooser();
572 _fileChooser.setDialogType(JFileChooser.SAVE_DIALOG);
573 _fileChooser.setFileFilter(new GenericFileFilter("filetype.svg", new String[] {"svg"}));
574 _fileChooser.setAcceptAllFileFilterUsed(false);
575 // start from directory in config which should be set
576 String configDir = Config.getConfigString(Config.KEY_TRACK_DIR);
577 if (configDir != null) {_fileChooser.setCurrentDirectory(new File(configDir));}
579 boolean chooseAgain = true;
583 if (_fileChooser.showSaveDialog(_parentFrame) == JFileChooser.APPROVE_OPTION)
585 // OK pressed and file chosen
586 File file = _fileChooser.getSelectedFile();
587 // Check file extension
588 if (!file.getName().toLowerCase().endsWith(".svg")) {
589 file = new File(file.getAbsolutePath() + ".svg");
591 // Check if file exists and if necessary prompt for overwrite
592 Object[] buttonTexts = {I18nManager.getText("button.overwrite"), I18nManager.getText("button.cancel")};
593 if (!file.exists() || (file.canWrite() && JOptionPane.showOptionDialog(_parentFrame,
594 I18nManager.getText("dialog.save.overwrite.text"),
595 I18nManager.getText("dialog.save.overwrite.title"), JOptionPane.YES_NO_OPTION,
596 JOptionPane.WARNING_MESSAGE, null, buttonTexts, buttonTexts[1])
597 == JOptionPane.YES_OPTION))
604 // Cancel pressed so no file selected
610 * @param inNumCharts number of charts to draw
611 * @return array of ints describing position and height of each subchart
613 private static int[] getHeights(int inNumCharts)
615 if (inNumCharts <= 1) {return new int[] {0, 100};}
616 if (inNumCharts == 2) {return new int[] {25, 75, 0, 25};}
617 if (inNumCharts == 3) {return new int[] {40, 60, 20, 20, 0, 20};}
618 return new int[] {54, 46, 36, 18, 18, 18, 0, 18};