+++ /dev/null
-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};
- }
-}