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};
}
}