content: package tim.prune.function.charts;
  
  import java.awt.BorderLayout;
  import java.awt.FlowLayout;
  import java.awt.GridLayout;
  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.data.Unit;
  import tim.prune.data.UnitSet;
  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(getConfig(), ExternalTools.TOOL_GNUPLOT))
  		{
  			_app.showErrorMessage(getNameKey(), "dialog.charts.gnuplotnotfound");
  			return;
  		}
  		// Make dialog window
  		if (_dialog == null)
  		{
  			_dialog = new JDialog(_parentFrame, getName(), 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 = (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(e -> {
  			showChart(_app.getTrackInfo().getTrack());
  			_dialog.setVisible(false);
  		});
  		buttonPanel.add(okButton);
  		// Cancel button
  		JButton cancelButton = new JButton(I18nManager.getText("button.cancel"));
  		cancelButton.addActionListener(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(false);
  		}
  	}
  
  	/**
  	 * Show the chart for the specified track
  	 * @param inTrack track object containing data
  	 */
  	private void showChart(Track inTrack)
  	{
  		int numCharts = 0;
  		for (JCheckBox yAxesBox : _yAxesBoxes) {
  			if (yAxesBox.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 = getConfig().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 void writeChart(OutputStreamWriter inWriter, Track inTrack, boolean inDistance, int inYaxis)
  	throws IOException
  	{
  		ChartSeries distValues = getDistanceValues(inTrack);
  		// Choose x values according to axis
  		final ChartSeries xValues;
  		if (inDistance) {
  			xValues = distValues;
  		}
  		else {
  			xValues = getTimeValues(inTrack);
  		}
  		// Choose y values according to axis
  		final ChartSeries yValues;
  		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;
  			default:
  				throw new IllegalArgumentException("y axis must be 0, 1, 2 or 3");
  		}
  		// 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
  		try (FileWriter 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");
  				}
  			}
  		}
  
  		// Sort out units to use
  		UnitSet unitSet = getConfig().getUnitSet();
  		final String distLabel = I18nManager.getText(unitSet.getDistanceUnit().getShortnameKey());
  		final String altLabel  = I18nManager.getText(unitSet.getAltitudeUnit().getShortnameKey());
  		final String speedLabel = I18nManager.getText(unitSet.getSpeedUnit().getShortnameKey());
  		final String vertSpeedLabel = I18nManager.getText(unitSet.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 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;
  		Unit distUnit = getConfig().getUnitSet().getDistanceUnit();
  		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, distUnit));
  
  			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;
  		for (int i=0; i<inTrack.getNumPoints(); i++)
  		{
  			DataPoint 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 ChartSeries getAltitudeValues(Track inTrack)
  	{
  		ChartSeries values = new ChartSeries(inTrack.getNumPoints());
  		final double multFactor = getConfig().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 ChartSeries getSpeedValues(Track inTrack)
  	{
  		// Calculate speeds using the same formula as the profile chart
  		SpeedData speeds = new SpeedData(inTrack);
  		speeds.init(getConfig().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 ChartSeries getVertSpeedValues(Track inTrack)
  	{
  		// Calculate speeds using the same formula as the profile chart
  		VerticalSpeedData speeds = new VerticalSpeedData(inTrack);
  		speeds.init(getConfig().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("filetypefilter.svg", new String[] {"svg"}));
  			_fileChooser.setAcceptAllFileFilterUsed(false);
  			// start from directory in config which should be set
  			String configDir = getConfig().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};
  	}
  }
  