content: package tim.prune.save;
  
  import java.awt.BorderLayout;
  import java.awt.CardLayout;
  import java.awt.Component;
  import java.awt.Dimension;
  import java.awt.FlowLayout;
  import java.awt.GridLayout;
  import java.io.File;
  import java.io.FileWriter;
  import java.io.IOException;
  import java.util.ArrayList;
  
  import javax.swing.BorderFactory;
  import javax.swing.Box;
  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.JFrame;
  import javax.swing.JLabel;
  import javax.swing.JOptionPane;
  import javax.swing.JPanel;
  import javax.swing.JRadioButton;
  import javax.swing.JScrollPane;
  import javax.swing.JTable;
  import javax.swing.JTextField;
  import javax.swing.ListSelectionModel;
  import javax.swing.table.TableModel;
  
  import tim.prune.App;
  import tim.prune.I18nManager;
  import tim.prune.UpdateMessageBroker;
  import tim.prune.config.Config;
  import tim.prune.data.Coordinate;
  import tim.prune.data.DataPoint;
  import tim.prune.data.Field;
  import tim.prune.data.FieldList;
  import tim.prune.data.Timestamp;
  import tim.prune.data.Track;
  import tim.prune.data.Unit;
  import tim.prune.data.UnitSetLibrary;
  import tim.prune.load.GenericFileFilter;
  import tim.prune.load.OneCharDocument;
  
  /**
   * Class to manage the saving of track data
   * as text into a user-specified file
   */
  public class FileSaver
  {
  	private final App _app;
  	private final JFrame _parentFrame;
  	private JDialog _dialog = null;
  	private JFileChooser _fileChooser = null;
  	private JPanel _cards = null;
  	private JButton _nextButton = null, _backButton = null;
  	private JTable _table = null;
  	private FieldSelectionTableModel _model = null;
  	private UpDownToggler _toggler = null;
  	private JRadioButton[] _delimiterRadios = null;
  	private JTextField _otherDelimiterText = null;
  	private JCheckBox _headerRowCheckbox = null;
  	private PointTypeSelector _pointTypeSelector = null;
  	private JRadioButton[] _coordUnitsRadios = null;
  	private JRadioButton[] _altitudeUnitsRadios = null;
  	private JRadioButton[] _timestampUnitsRadios = null;
  
  	private static final Coordinate.Format[] FORMAT_COORDS = {Coordinate.Format.NONE, Coordinate.Format.DEG_MIN_SEC,
  		Coordinate.Format.DEG_MIN, Coordinate.Format.DEG};
  	private static final Unit[] UNIT_ALTS = {null, UnitSetLibrary.UNITS_METRES, UnitSetLibrary.UNITS_FEET};
  	private static final Timestamp.Format[] FORMAT_TIMES = {Timestamp.Format.ORIGINAL, Timestamp.Format.LOCALE, Timestamp.Format.ISO8601};
  
  
  	/**
  	 * Constructor
  	 * @param inApp application object to inform of success
  	 * @param inParentFrame parent frame
  	 */
  	public FileSaver(App inApp, JFrame inParentFrame)
  	{
  		_app = inApp;
  		_parentFrame = inParentFrame;
  	}
  
  
  	/**
  	 * Show the save file dialog
  	 * @param inDefaultDelimiter default delimiter to use
  	 */
  	public void showDialog(char inDefaultDelimiter)
  	{
  		if (_dialog == null)
  		{
  			_dialog = new JDialog(_parentFrame, I18nManager.getText("dialog.saveoptions.title"), true);
  			_dialog.setLocationRelativeTo(_parentFrame);
  			_dialog.getContentPane().add(makeDialogComponents());
  			_dialog.pack();
  		}
  
  		// Check field list
  		Track track = _app.getTrackInfo().getTrack();
  		FieldList fieldList = track.getFieldList();
  		final int numFields = fieldList.getNumFields();
  		ArrayList<FieldInfo> fieldInfos = new ArrayList<>();
  		for (int i=0; i<numFields; i++)
  		{
  			Field field = fieldList.getField(i);
  			fieldInfos.add(new FieldInfo(field, track.hasData(field)));
  		}
  		// Add fields for photos / audios if present
  		if (_app.getTrackInfo().getPhotoList().hasCorrelatedMedia()) {
  			fieldInfos.add(new FieldInfo(Field.PHOTO, true));
  		}
  		if (_app.getTrackInfo().getAudioList().hasCorrelatedMedia()) {
  			fieldInfos.add(new FieldInfo(Field.AUDIO, true));
  		}
  		_model = new FieldSelectionTableModel(fieldInfos);
  		// Initialise dialog and show it
  		initDialog(_model, inDefaultDelimiter);
  		_dialog.setVisible(true);
  	}
  
  
  	/**
  	 * Make the dialog components
  	 * @return the GUI components for the save dialog
  	 */
  	private Component makeDialogComponents()
  	{
  		JPanel panel = new JPanel();
  		panel.setLayout(new BorderLayout());
  		_cards = new JPanel();
  		_cards.setLayout(new CardLayout());
  		panel.add(_cards, BorderLayout.CENTER);
  
  		// Make first card for field selection and delimiter
  		JPanel firstCard = new JPanel();
  		firstCard.setLayout(new BoxLayout(firstCard, BoxLayout.Y_AXIS));
  		JPanel tablePanel = new JPanel();
  		tablePanel.setLayout(new BorderLayout());
  		_table = new JTable();
  		_table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
  		// Enclose table in a scrollpane to prevent other components getting lost
  		JScrollPane scrollPane = new JScrollPane(_table);
  		_table.setPreferredScrollableViewportSize(new Dimension(300, 150));
  		tablePanel.add(scrollPane, BorderLayout.CENTER);
  
  		// Make a panel to hold the table and up/down buttons
  		JPanel fieldsPanel = new JPanel();
  		fieldsPanel.setLayout(new BorderLayout());
  		fieldsPanel.add(tablePanel, BorderLayout.CENTER);
  		JPanel updownPanel = new JPanel();
  		updownPanel.setLayout(new BoxLayout(updownPanel, BoxLayout.Y_AXIS));
  		JButton moveUpButton = new JButton(I18nManager.getText("button.moveup"));
  		moveUpButton.addActionListener(e -> onMoveUp());
  		moveUpButton.setEnabled(false);
  		updownPanel.add(moveUpButton);
  		JButton moveDownButton = new JButton(I18nManager.getText("button.movedown"));
  		moveDownButton.addActionListener(e -> onMoveDown());
  		moveDownButton.setEnabled(false);
  		updownPanel.add(moveDownButton);
  		fieldsPanel.add(updownPanel, BorderLayout.EAST);
  		// enable/disable buttons based on table row selection
  		_toggler = new UpDownToggler(moveUpButton, moveDownButton);
  		_table.getSelectionModel().addListSelectionListener(_toggler);
  
  		// Add fields panel and the delimiter panel to first card in pack
  		JLabel saveOptionsLabel = new JLabel(I18nManager.getText("dialog.save.fieldstosave"));
  		saveOptionsLabel.setAlignmentX(JLabel.LEFT_ALIGNMENT);
  		firstCard.add(saveOptionsLabel);
  		fieldsPanel.setAlignmentX(JPanel.LEFT_ALIGNMENT);
  		firstCard.add(fieldsPanel);
  		firstCard.add(Box.createRigidArea(new Dimension(0,10)));
  
  		// delimiter panel
  		JLabel delimLabel = new JLabel(I18nManager.getText("dialog.delimiter.label"));
  		delimLabel.setAlignmentX(JLabel.LEFT_ALIGNMENT);
  		firstCard.add(delimLabel);
  		JPanel delimsPanel = new JPanel();
  		delimsPanel.setLayout(new GridLayout(0, 2));
  		delimsPanel.setAlignmentX(JPanel.LEFT_ALIGNMENT);
  		// radio buttons
  		_delimiterRadios = new JRadioButton[5];
  		_delimiterRadios[0] = new JRadioButton(I18nManager.getText("dialog.delimiter.comma"));
  		delimsPanel.add(_delimiterRadios[0]);
  		_delimiterRadios[1] = new JRadioButton(I18nManager.getText("dialog.delimiter.tab"));
  		delimsPanel.add(_delimiterRadios[1]);
  		_delimiterRadios[2] = new JRadioButton(I18nManager.getText("dialog.delimiter.semicolon"));
  		delimsPanel.add(_delimiterRadios[2]);
  		_delimiterRadios[3] = new JRadioButton(I18nManager.getText("dialog.delimiter.space"));
  		delimsPanel.add(_delimiterRadios[3]);
  		JPanel otherPanel = new JPanel();
  		otherPanel.setLayout(new FlowLayout(FlowLayout.LEFT, 0, 0));
  		_delimiterRadios[4] = new JRadioButton(I18nManager.getText("dialog.delimiter.other"));
  		otherPanel.add(_delimiterRadios[4]);
  		_otherDelimiterText = new JTextField(new OneCharDocument(), null, 2);
  		otherPanel.add(_otherDelimiterText);
  		// Group radio buttons
  		ButtonGroup delimGroup = new ButtonGroup();
  		for (JRadioButton radio : _delimiterRadios) {
  			delimGroup.add(radio);
  		}
  		delimsPanel.add(otherPanel);
  		firstCard.add(delimsPanel);
  
  		// header checkbox
  		firstCard.add(Box.createRigidArea(new Dimension(0,10)));
  		_headerRowCheckbox = new JCheckBox(I18nManager.getText("dialog.save.headerrow"), true);
  		firstCard.add(_headerRowCheckbox);
  		_cards.add(firstCard, "card1");
  
  		// Second card
  		JPanel secondCard = new JPanel();
  		secondCard.setLayout(new BorderLayout());
  		JPanel secondCardHolder = new JPanel();
  		secondCardHolder.setLayout(new BoxLayout(secondCardHolder, BoxLayout.Y_AXIS));
  		// point type selector
  		secondCardHolder.add(Box.createRigidArea(new Dimension(0,10)));
  		_pointTypeSelector = new PointTypeSelector();
  		_pointTypeSelector.setAlignmentX(JLabel.LEFT_ALIGNMENT);
  		secondCardHolder.add(_pointTypeSelector);
  		JLabel coordLabel = new JLabel(I18nManager.getText("dialog.save.coordinateunits"));
  		coordLabel.setAlignmentX(JLabel.LEFT_ALIGNMENT);
  		secondCardHolder.add(coordLabel);
  		JPanel coordsUnitsPanel = new JPanel();
  		coordsUnitsPanel.setBorder(BorderFactory.createEtchedBorder());
  		coordsUnitsPanel.setLayout(new GridLayout(0, 2));
  		_coordUnitsRadios = new JRadioButton[4];
  		_coordUnitsRadios[0] = new JRadioButton(I18nManager.getText("units.original"));
  		_coordUnitsRadios[1] = new JRadioButton(I18nManager.getText("units.degminsec"));
  		_coordUnitsRadios[2] = new JRadioButton(I18nManager.getText("units.degmin"));
  		_coordUnitsRadios[3] = new JRadioButton(I18nManager.getText("units.deg"));
  		ButtonGroup coordGroup = new ButtonGroup();
  		for (int i=0; i<4; i++)
  		{
  			coordGroup.add(_coordUnitsRadios[i]);
  			coordsUnitsPanel.add(_coordUnitsRadios[i]);
  			_coordUnitsRadios[i].setSelected(i==0);
  		}
  		coordsUnitsPanel.setAlignmentX(JPanel.LEFT_ALIGNMENT);
  		secondCardHolder.add(coordsUnitsPanel);
  		secondCardHolder.add(Box.createRigidArea(new Dimension(0,7)));
  		// altitude units
  		JLabel altUnitsLabel = new JLabel(I18nManager.getText("dialog.save.altitudeunits"));
  		altUnitsLabel.setAlignmentX(JLabel.LEFT_ALIGNMENT);
  		secondCardHolder.add(altUnitsLabel);
  		JPanel altUnitsPanel = new JPanel();
  		altUnitsPanel.setBorder(BorderFactory.createEtchedBorder());
  		altUnitsPanel.setLayout(new GridLayout(0, 2));
  		_altitudeUnitsRadios = new JRadioButton[3];
  		_altitudeUnitsRadios[0] = new JRadioButton(I18nManager.getText("units.original"));
  		_altitudeUnitsRadios[1] = new JRadioButton(I18nManager.getText("units.metres"));
  		_altitudeUnitsRadios[2] = new JRadioButton(I18nManager.getText("units.feet"));
  		ButtonGroup altGroup = new ButtonGroup();
  		for (int i=0; i<3; i++)
  		{
  			altGroup.add(_altitudeUnitsRadios[i]);
  			altUnitsPanel.add(_altitudeUnitsRadios[i]);
  			_altitudeUnitsRadios[i].setSelected(i==0);
  		}
  		altUnitsPanel.setAlignmentX(JPanel.LEFT_ALIGNMENT);
  		secondCardHolder.add(altUnitsPanel);
  		secondCardHolder.add(Box.createRigidArea(new Dimension(0,7)));
  		// Selection of format of timestamps
  		JLabel timestampLabel = new JLabel(I18nManager.getText("dialog.save.timestampformat"));
  		timestampLabel.setAlignmentX(JLabel.LEFT_ALIGNMENT);
  		secondCardHolder.add(timestampLabel);
  		JPanel timestampPanel = new JPanel();
  		timestampPanel.setBorder(BorderFactory.createEtchedBorder());
  		timestampPanel.setLayout(new GridLayout(0, 2));
  		_timestampUnitsRadios = new JRadioButton[3];
  		_timestampUnitsRadios[0] = new JRadioButton(I18nManager.getText("units.original"));
  		_timestampUnitsRadios[1] = new JRadioButton(I18nManager.getText("units.default"));
  		_timestampUnitsRadios[2] = new JRadioButton(I18nManager.getText("units.iso8601"));
  		ButtonGroup timeGroup = new ButtonGroup();
  		for (int i=0; i<3; i++)
  		{
  			timeGroup.add(_timestampUnitsRadios[i]);
  			timestampPanel.add(_timestampUnitsRadios[i]);
  			_timestampUnitsRadios[i].setSelected(i==0);
  		}
  		timestampPanel.setAlignmentX(JPanel.LEFT_ALIGNMENT);
  		secondCardHolder.add(timestampPanel);
  		secondCard.add(secondCardHolder, BorderLayout.NORTH);
  		_cards.add(secondCard, "card2");
  
  		// Put together with ok/cancel buttons on the bottom
  		JPanel buttonPanel = new JPanel();
  		buttonPanel.setLayout(new FlowLayout(FlowLayout.RIGHT));
  		_backButton = new JButton(I18nManager.getText("button.back"));
  		_backButton.addActionListener(e -> onBackPressed());
  		_backButton.setEnabled(false);
  		buttonPanel.add(_backButton);
  		_nextButton = new JButton(I18nManager.getText("button.next"));
  		_nextButton.setEnabled(true);
  		_nextButton.addActionListener(e -> onNextPressed());
  		buttonPanel.add(_nextButton);
  		JButton okButton = new JButton(I18nManager.getText("button.finish"));
  		okButton.addActionListener(e -> saveToFile());
  		buttonPanel.add(okButton);
  		JButton cancelButton = new JButton(I18nManager.getText("button.cancel"));
  		cancelButton.addActionListener(e -> _dialog.dispose());
  		buttonPanel.add(cancelButton);
  		panel.add(buttonPanel, BorderLayout.SOUTH);
  		return panel;
  	}
  
  	/** Move the selected row up */
  	private void onMoveUp()
  	{
  		final int row = _table.getSelectedRow();
  		if (row > 0)
  		{
  			_model.swapItems(row, row - 1);
  			_table.setRowSelectionInterval(row - 1, row - 1);
  		}
  	}
  
  	/** Move the selected row down */
  	private void onMoveDown()
  	{
  		final int row = _table.getSelectedRow();
  		if (row > -1 && row < (_model.getRowCount() - 1))
  		{
  			_model.swapItems(row, row + 1);
  			_table.setRowSelectionInterval(row + 1, row + 1);
  		}
  	}
  
  	/** Go back to previous card */
  	private void onBackPressed()
  	{
  		CardLayout cl = (CardLayout) _cards.getLayout();
  		cl.previous(_cards);
  		_backButton.setEnabled(false);
  		_nextButton.setEnabled(true);
  	}
  
  	/** Go forward to next card */
  	private void onNextPressed()
  	{
  		CardLayout cl = (CardLayout) _cards.getLayout();
  		cl.next(_cards);
  		_backButton.setEnabled(true);
  		_nextButton.setEnabled(false);
  	}
  
  	/**
  	 * Initialize the dialog with the given details
  	 * @param inModel table model
  	 * @param inDefaultDelimiter default delimiter character
  	 */
  	private void initDialog(TableModel inModel, char inDefaultDelimiter)
  	{
  		// set table model
  		_table.setModel(inModel);
  		// reset toggler
  		_toggler.setListSize(inModel.getRowCount());
  		// choose last-used delimiter as default
  		switch (inDefaultDelimiter)
  		{
  			case ','  : _delimiterRadios[0].setSelected(true); break;
  			case '\t' : _delimiterRadios[1].setSelected(true); break;
  			case ';'  : _delimiterRadios[2].setSelected(true); break;
  			case ' '  : _delimiterRadios[3].setSelected(true); break;
  			default   : _delimiterRadios[4].setSelected(true);
  						_otherDelimiterText.setText("" + inDefaultDelimiter);
  		}
  		_pointTypeSelector.init(_app.getTrackInfo());
  		// set card and enable buttons
  		CardLayout cl = (CardLayout) _cards.getLayout();
  		cl.first(_cards);
  		_nextButton.setEnabled(true);
  		_backButton.setEnabled(false);
  	}
  
  
  	/**
  	 * Start the save process by choosing the file to save to
  	 */
  	private void saveToFile()
  	{
  		if (!_pointTypeSelector.getAnythingSelected())
  		{
  			JOptionPane.showMessageDialog(_parentFrame, I18nManager.getText("dialog.save.notypesselected"),
  				I18nManager.getText("dialog.saveoptions.title"), JOptionPane.WARNING_MESSAGE);
  			return;
  		}
  		if (_fileChooser == null)
  		{
  			_fileChooser = new JFileChooser();
  			_fileChooser.setDialogType(JFileChooser.SAVE_DIALOG);
  			_fileChooser.addChoosableFileFilter(new GenericFileFilter("filetypefilter.txt", new String[] {"txt", "text"}));
  			_fileChooser.setAcceptAllFileFilterUsed(true);
  			// start from directory in config which should be set
  			Config config = _app.getConfig();
  			String configDir = config.getConfigString(Config.KEY_TRACK_DIR);
  			if (configDir == null) {configDir = config.getConfigString(Config.KEY_PHOTO_DIR);}
  			if (configDir != null) {
  				_fileChooser.setCurrentDirectory(new File(configDir));
  			}
  		}
  		boolean shouldClose = true;
  		if (_fileChooser.showSaveDialog(_parentFrame) == JFileChooser.APPROVE_OPTION) {
  			shouldClose = saveToFile(_fileChooser.getSelectedFile());
  		}
  		if (shouldClose) {
  			_dialog.dispose();
  		}
  	}
  
  
  	/**
  	 * Save the track to the specified file using the chosen options
  	 * @param inSaveFile file to save to
  	 * @return true if save successful, false if failed
  	 */
  	private boolean saveToFile(File inSaveFile)
  	{
  		// TODO: Shorten method
  		final String lineSeparator = System.getProperty("line.separator");
  		boolean saveOK = true;
  		// Get coordinate format and altitude format
  		Coordinate.Format coordFormat = Coordinate.Format.NONE;
  		for (int i=0; i<_coordUnitsRadios.length; i++) {
  			if (_coordUnitsRadios[i].isSelected()) {
  				coordFormat = FORMAT_COORDS[i];
  			}
  		}
  		Unit altitudeUnit = null;
  		for (int i=0; i<_altitudeUnitsRadios.length; i++)
  		{
  			if (_altitudeUnitsRadios[i].isSelected()) {
  				altitudeUnit = UNIT_ALTS[i];
  			}
  		}
  		// Get timestamp format
  		Timestamp.Format timestampFormat = Timestamp.Format.ORIGINAL;
  		for (int i=0; i<_timestampUnitsRadios.length; i++)
  		{
  			if (_timestampUnitsRadios[i].isSelected()) {
  				timestampFormat = FORMAT_TIMES[i];
  			}
  		}
  
  		// Correct chosen filename if necessary
  		final File saveFile = (isFilenameOk(inSaveFile)?inSaveFile:new File(inSaveFile.getAbsolutePath() + ".txt"));
  
  		// Check if file exists, and confirm overwrite if necessary
  		Object[] buttonTexts = {I18nManager.getText("button.overwrite"), I18nManager.getText("button.cancel")};
  		if (!saveFile.exists() || 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)
  		{
  			try (FileWriter writer = new FileWriter(saveFile))
  			{
  				// Determine delimiter character to use
  				final char delimiter = getDelimiter();
  				FieldInfo info = null;
  
  				int numFields = _model.getRowCount();
  				boolean firstField = true;
  				// Write header row if required
  				if (_headerRowCheckbox.isSelected())
  				{
  					StringBuilder buffer = new StringBuilder();
  					for (int f=0; f<numFields; f++)
  					{
  						info = _model.getFieldInfo(f);
  						if (info.isSelected())
  						{
  							// output field separator
  							if (!firstField) {
  								buffer.append(delimiter);
  							}
  							buffer.append(info.getField().getName());
  							firstField = false;
  						}
  					}
  					writer.write(buffer.toString());
  					writer.write(lineSeparator);
  				}
  
  				// Examine selection
  				int selStart = -1, selEnd = -1;
  				if (_pointTypeSelector.getJustSelection()) {
  					selStart = _app.getTrackInfo().getSelection().getStart();
  					selEnd = _app.getTrackInfo().getSelection().getEnd();
  				}
  				// Loop over points outputting each in turn to buffer
  				Track track = _app.getTrackInfo().getTrack();
  				final int numPoints = track.getNumPoints();
  				int numSaved = 0;
  				for (int p=0; p<numPoints; p++)
  				{
  					DataPoint point = track.getPoint(p);
  					boolean savePoint = ((point.isWaypoint() && _pointTypeSelector.getWaypointsSelected())
  						|| (!point.isWaypoint() && !point.hasMedia() && _pointTypeSelector.getTrackpointsSelected())
  						|| (!point.isWaypoint() && point.getPhoto()!=null && _pointTypeSelector.getPhotopointsSelected())
  						|| (!point.isWaypoint() && point.getAudio()!=null && _pointTypeSelector.getAudiopointsSelected()))
  						&& (!_pointTypeSelector.getJustSelection() || (p>=selStart && p<=selEnd));
  					if (!savePoint) {continue;}
  					numSaved++;
  					firstField = true;
  					StringBuffer buffer = new StringBuffer();
  					for (int f=0; f<numFields; f++)
  					{
  						info = _model.getFieldInfo(f);
  						if (info.isSelected())
  						{
  							// output field separator
  							if (!firstField) {
  								buffer.append(delimiter);
  							}
  							saveField(buffer, point, info.getField(), coordFormat, altitudeUnit, timestampFormat);
  							firstField = false;
  						}
  					}
  					// Output to file
  					writer.write(buffer.toString());
  					writer.write(lineSeparator);
  				}
  				// Store directory in config for later
  				_app.getConfig().setConfigString(Config.KEY_TRACK_DIR, saveFile.getParentFile().getAbsolutePath());
  				// Add to recent file list
  				_app.addRecentFile(inSaveFile, true);
  				// Save successful
  				UpdateMessageBroker.informSubscribers();
  				UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.save.ok1")
  					 + " " + numSaved + " " + I18nManager.getText("confirm.save.ok2")
  					 + " " + saveFile.getAbsolutePath());
  				_app.informDataSaved();
  			}
  			catch (IOException ioe)
  			{
  				saveOK = false;
  				_app.showErrorMessageNoLookup("error.save.dialogtitle",
  					I18nManager.getText("error.save.failed") + " : " + ioe.getMessage());
  			}
  		}
  		else
  		{
  			// Overwrite file confirm cancelled
  			saveOK = false;
  		}
  		return saveOK;
  	}
  
  
  	/**
  	 * Format the given field and append to the given buffer for saving
  	 * @param inBuffer buffer to append to
  	 * @param inPoint point object
  	 * @param inField field object
  	 * @param inCoordFormat coordinate format
  	 * @param inAltitudeUnit altitude unit
  	 * @param inTimestampFormat timestamp format
  	 */
  	private void saveField(StringBuffer inBuffer, DataPoint inPoint, Field inField,
  		Coordinate.Format inCoordFormat, Unit inAltitudeUnit, Timestamp.Format inTimestampFormat)
  	{
  		// Output field according to type
  		if (inField == Field.LATITUDE)
  		{
  			inBuffer.append(inPoint.getLatitude().output(inCoordFormat));
  		}
  		else if (inField == Field.LONGITUDE)
  		{
  			inBuffer.append(inPoint.getLongitude().output(inCoordFormat));
  		}
  		else if (inField == Field.ALTITUDE)
  		{
  			if (inPoint.hasAltitude()) {
  				inBuffer.append(inPoint.getAltitude().getStringValue(inAltitudeUnit));
  			}
  		}
  		else if (inField == Field.TIMESTAMP)
  		{
  			if (inPoint.hasTimestamp())	{
  				inBuffer.append(inPoint.getTimestamp().getText(inTimestampFormat, null));
  			}
  		}
  		else if (inField == Field.PHOTO)
  		{
  			if (inPoint.getPhoto() != null)	{
  				inBuffer.append(inPoint.getPhoto().getName());
  			}
  		}
  		else if (inField == Field.AUDIO)
  		{
  			if (inPoint.getAudio() != null) {
  				inBuffer.append(inPoint.getAudio().getName());
  			}
  		}
  		else
  		{
  			String value = inPoint.getFieldValue(inField);
  			if (value != null) {
  				inBuffer.append(value);
  			}
  		}
  	}
  
  
  	/**
  	 * @return the selected delimiter character
  	 */
  	private char getDelimiter()
  	{
  		// Check the preset 4 delimiters
  		final char[] delimiters = {',', '\t', ';', ' '};
  		for (int i=0; i<4; i++)
  		{
  			if (_delimiterRadios[i].isSelected()) {
  				return delimiters[i];
  			}
  		}
  		// Wasn't any of those so must be 'other'
  		return _otherDelimiterText.getText().charAt(0);
  	}
  
  
  	/**
  	 * Check the selected filename to see if it is acceptable
  	 * @param inFile chosen file to save
  	 * @return true if filename is ok
  	 */
  	private static boolean isFilenameOk(File inFile)
  	{
  		String filename = inFile.getName().toLowerCase();
  		return (filename.length() <4 || (!filename.endsWith(".gpx")
  			&& !filename.endsWith(".kml") && !filename.endsWith(".kmz") && !filename.endsWith(".zip")));
  	}
  }
  