]> gitweb.fperrin.net Git - GpsPrune.git/blobdiff - src/tim/prune/function/charts/Charter.java
Moved source into separate src directory due to popular request
[GpsPrune.git] / src / tim / prune / function / charts / Charter.java
diff --git a/src/tim/prune/function/charts/Charter.java b/src/tim/prune/function/charts/Charter.java
new file mode 100644 (file)
index 0000000..5b7ae83
--- /dev/null
@@ -0,0 +1,620 @@
+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<inTrack.getNumPoints(); i++) {
+                               if (xValues.hasData(i) && yValues.hasData(i)) {
+                                       tempFileWriter.write("" + xValues.getData(i) + ", " + yValues.getData(i) + "\n");
+                               }
+                       }
+               }
+               catch (IOException ioe) { // rethrow
+                       throw ioe;
+               }
+               finally {
+                       try {
+                               tempFileWriter.close();
+                       }
+                       catch (Exception e) {}
+               }
+
+               // Sort out units to use
+               final String distLabel = I18nManager.getText(Config.getUnitSet().getDistanceUnit().getShortnameKey());
+               final String altLabel  = I18nManager.getText(Config.getUnitSet().getAltitudeUnit().getShortnameKey());
+               final String speedLabel = I18nManager.getText(Config.getUnitSet().getSpeedUnit().getShortnameKey());
+               final String vertSpeedLabel = I18nManager.getText(Config.getUnitSet().getVerticalSpeedUnit().getShortnameKey());
+
+               // Set x axis label
+               if (inDistance) {
+                       inWriter.write("set xlabel '" + I18nManager.getText("fieldname.distance") + " (" + distLabel + ")'\n");
+               }
+               else {
+                       inWriter.write("set xlabel '" + I18nManager.getText("fieldname.time") + " (" + I18nManager.getText("units.hours") + ")'\n");
+               }
+
+               // set other labels and plot chart
+               String chartTitle = null;
+               switch (inYaxis)
+               {
+               case 0: // y axis is distance
+                       inWriter.write("set ylabel '" + I18nManager.getText("fieldname.distance") + " (" + distLabel + ")'\n");
+                       chartTitle = I18nManager.getText("fieldname.distance");
+                       break;
+               case 1: // y axis is altitude
+                       inWriter.write("set ylabel '" + I18nManager.getText("fieldname.altitude") + " (" + altLabel + ")'\n");
+                       chartTitle = I18nManager.getText("fieldname.altitude");
+                       break;
+               case 2: // y axis is speed
+                       inWriter.write("set ylabel '" + I18nManager.getText("fieldname.speed") + " (" + speedLabel + ")'\n");
+                       chartTitle = I18nManager.getText("fieldname.speed");
+                       break;
+               case 3: // y axis is vertical speed
+                       inWriter.write("set ylabel '" + I18nManager.getText("fieldname.verticalspeed") + " (" + vertSpeedLabel + ")'\n");
+                       chartTitle = I18nManager.getText("fieldname.verticalspeed");
+                       break;
+               }
+               inWriter.write("set style fill solid 0.5 border -1\n");
+               inWriter.write("plot '" + tempFile.getAbsolutePath() + "' title '" + chartTitle + "' with filledcurve y1=0 lt rgb \"#009000\"\n");
+       }
+
+
+       /**
+        * Calculate the distance values for each point in the given track
+        * @param inTrack track object
+        * @return distance values in a ChartSeries object
+        */
+       private static ChartSeries getDistanceValues(Track inTrack)
+       {
+               // Calculate distances and fill in in values array
+               ChartSeries values = new ChartSeries(inTrack.getNumPoints());
+               double totalRads = 0;
+               DataPoint prevPoint = null, currPoint = null;
+               for (int i=0; i<inTrack.getNumPoints(); i++)
+               {
+                       currPoint = inTrack.getPoint(i);
+                       if (prevPoint != null && !currPoint.isWaypoint() && !currPoint.getSegmentStart())
+                       {
+                               totalRads += DataPoint.calculateRadiansBetween(prevPoint, currPoint);
+                       }
+
+                       // distance values use currently configured units
+                       values.setData(i, Distance.convertRadiansToDistance(totalRads));
+
+                       prevPoint = currPoint;
+               }
+               return values;
+       }
+
+       /**
+        * Calculate the time values for each point in the given track
+        * @param inTrack track object
+        * @return time values in a ChartSeries object
+        */
+       private static ChartSeries getTimeValues(Track inTrack)
+       {
+               // Calculate times and fill in in values array
+               ChartSeries values = new ChartSeries(inTrack.getNumPoints());
+               double seconds = 0.0;
+               Timestamp prevTimestamp = null;
+               DataPoint currPoint = null;
+               for (int i=0; i<inTrack.getNumPoints(); i++)
+               {
+                       currPoint = inTrack.getPoint(i);
+                       if (currPoint.hasTimestamp())
+                       {
+                               if (!currPoint.getSegmentStart() && prevTimestamp != null) {
+                                       seconds += (currPoint.getTimestamp().getMillisecondsSince(prevTimestamp) / 1000.0);
+                               }
+                               values.setData(i, seconds / 60.0 / 60.0);
+                               prevTimestamp = currPoint.getTimestamp();
+                       }
+               }
+               return values;
+       }
+
+       /**
+        * Calculate the altitude values for each point in the given track
+        * @param inTrack track object
+        * @return altitude values in a ChartSeries object
+        */
+       private static ChartSeries getAltitudeValues(Track inTrack)
+       {
+               ChartSeries values = new ChartSeries(inTrack.getNumPoints());
+               final double multFactor = Config.getUnitSet().getAltitudeUnit().getMultFactorFromStd();
+               for (int i=0; i<inTrack.getNumPoints(); i++) {
+                       if (inTrack.getPoint(i).hasAltitude()) {
+                               values.setData(i, inTrack.getPoint(i).getAltitude().getMetricValue() * multFactor);
+                       }
+               }
+               return values;
+       }
+
+       /**
+        * Calculate the speed values for each point in the given track
+        * @param inTrack track object
+        * @return speed values in a ChartSeries object
+        */
+       private static ChartSeries getSpeedValues(Track inTrack)
+       {
+               // Calculate speeds using the same formula as the profile chart
+               SpeedData speeds = new SpeedData(inTrack);
+               speeds.init(Config.getUnitSet());
+
+               final int numPoints = inTrack.getNumPoints();
+               ChartSeries values = new ChartSeries(numPoints);
+               // Loop over collected points
+               for (int i=0; i<numPoints; i++)
+               {
+                       if (speeds.hasData(i))
+                       {
+                               values.setData(i, speeds.getData(i));
+                       }
+               }
+               return values;
+       }
+
+       /**
+        * Calculate the vertical speed values for each point in the given track
+        * @param inTrack track object
+        * @return vertical speed values in a ChartSeries object
+        */
+       private static ChartSeries getVertSpeedValues(Track inTrack)
+       {
+               // Calculate speeds using the same formula as the profile chart
+               VerticalSpeedData speeds = new VerticalSpeedData(inTrack);
+               speeds.init(Config.getUnitSet());
+
+               final int numPoints = inTrack.getNumPoints();
+               ChartSeries values = new ChartSeries(numPoints);
+               // Loop over collected points
+               for (int i=0; i<numPoints; i++)
+               {
+                       if (speeds.hasData(i))
+                       {
+                               values.setData(i, speeds.getData(i));
+                       }
+               }
+               return values;
+       }
+
+
+       /**
+        * Select a file to write for the SVG output
+        * @return selected File object or null if cancelled
+        */
+       private File selectSvgFile()
+       {
+               if (_fileChooser == null)
+               {
+                       _fileChooser = new JFileChooser();
+                       _fileChooser.setDialogType(JFileChooser.SAVE_DIALOG);
+                       _fileChooser.setFileFilter(new GenericFileFilter("filetype.svg", new String[] {"svg"}));
+                       _fileChooser.setAcceptAllFileFilterUsed(false);
+                       // start from directory in config which should be set
+                       String configDir = Config.getConfigString(Config.KEY_TRACK_DIR);
+                       if (configDir != null) {_fileChooser.setCurrentDirectory(new File(configDir));}
+               }
+               boolean chooseAgain = true;
+               while (chooseAgain)
+               {
+                       chooseAgain = false;
+                       if (_fileChooser.showSaveDialog(_parentFrame) == JFileChooser.APPROVE_OPTION)
+                       {
+                               // OK pressed and file chosen
+                               File file = _fileChooser.getSelectedFile();
+                               // Check file extension
+                               if (!file.getName().toLowerCase().endsWith(".svg")) {
+                                       file = new File(file.getAbsolutePath() + ".svg");
+                               }
+                               // Check if file exists and if necessary prompt for overwrite
+                               Object[] buttonTexts = {I18nManager.getText("button.overwrite"), I18nManager.getText("button.cancel")};
+                               if (!file.exists() || (file.canWrite() && JOptionPane.showOptionDialog(_parentFrame,
+                                               I18nManager.getText("dialog.save.overwrite.text"),
+                                               I18nManager.getText("dialog.save.overwrite.title"), JOptionPane.YES_NO_OPTION,
+                                               JOptionPane.WARNING_MESSAGE, null, buttonTexts, buttonTexts[1])
+                                       == JOptionPane.YES_OPTION))
+                               {
+                                       return file;
+                               }
+                               chooseAgain = true;
+                       }
+               }
+               // Cancel pressed so no file selected
+               return null;
+       }
+
+
+       /**
+        * @param inNumCharts number of charts to draw
+        * @return array of ints describing position and height of each subchart
+        */
+       private static int[] getHeights(int inNumCharts)
+       {
+               if (inNumCharts <= 1) {return new int[] {0, 100};}
+               if (inNumCharts == 2) {return new int[] {25, 75, 0, 25};}
+               if (inNumCharts == 3) {return new int[] {40, 60, 20, 20, 0, 20};}
+               return new int[] {54, 46, 36, 18, 18, 18, 0, 18};
+       }
+}