package tim.prune.save;

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.util.Iterator;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

import javax.imageio.ImageIO;
import javax.imageio.ImageWriter;
import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.ButtonGroup;
import javax.swing.ImageIcon;
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.JProgressBar;
import javax.swing.JRadioButton;
import javax.swing.JTextField;
import javax.swing.SwingConstants;

import tim.prune.App;
import tim.prune.GenericFunction;
import tim.prune.I18nManager;
import tim.prune.UpdateMessageBroker;
import tim.prune.config.ColourUtils;
import tim.prune.config.Config;
import tim.prune.data.Coordinate;
import tim.prune.data.DataPoint;
import tim.prune.data.Field;
import tim.prune.data.RecentFile;
import tim.prune.data.Timestamp;
import tim.prune.data.Track;
import tim.prune.data.TrackInfo;
import tim.prune.data.UnitSetLibrary;
import tim.prune.gui.DialogCloser;
import tim.prune.gui.ImageUtils;
import tim.prune.gui.WholeNumberField;
import tim.prune.gui.colour.ColourChooser;
import tim.prune.gui.colour.ColourPatch;
import tim.prune.load.GenericFileFilter;
import tim.prune.save.xml.XmlUtils;

/**
 * Class to export track information
 * into a specified Kml or Kmz file
 */
public class KmlExporter extends GenericFunction implements Runnable
{
	private TrackInfo _trackInfo = null;
	private Track _track = null;
	private JDialog _dialog = null;
	private JTextField _descriptionField = null;
	private PointTypeSelector _pointTypeSelector = null;
	private JRadioButton _gxExtensionsRadio = null;
	private JCheckBox _altitudesCheckbox = null;
	private JCheckBox _kmzCheckbox = null;
	private JCheckBox _exportImagesCheckbox = null;
	private JLabel _imageSizeLabel = null;
	private WholeNumberField _imageSizeField = null;
	private ColourPatch _colourPatch = null;
	private JLabel _progressLabel = null;
	private JProgressBar _progressBar = null;
	private Dimension[] _imageDimensions = null;
	private JFileChooser _fileChooser = null;
	private File _exportFile = null;
	private JButton _okButton = null;
	private boolean _cancelPressed = false;
	private ColourChooser _colourChooser = null;

	// Filename of Kml file within zip archive
	private static final String KML_FILENAME_IN_KMZ = "doc.kml";
	// Default width and height of thumbnail images in Kmz
	private static final int DEFAULT_THUMBNAIL_WIDTH = 240;
	// Default track colour
	private static final Color DEFAULT_TRACK_COLOUR = new Color(204, 0, 0); // red


	/**
	 * Constructor
	 * @param inApp app object
	 */
	public KmlExporter(App inApp)
	{
		super(inApp);
		_trackInfo = inApp.getTrackInfo();
		_track = _trackInfo.getTrack();
	}

	/** Get name key */
	public String getNameKey() {
		return "function.exportkml";
	}

	/**
	 * Show the dialog to select options and export file
	 */
	public void begin()
	{
		// Make dialog window including whether to compress to kmz (and include pictures) or not
		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();
			_colourChooser = new ColourChooser(_dialog);
		}
		// Fill in image size from config
		_imageSizeField.setValue(Config.getConfigInt(Config.KEY_KMZ_IMAGE_SIZE));
		enableCheckboxes();
		_descriptionField.setEnabled(true);
		_okButton.setEnabled(true);
		_progressLabel.setText("");
		_progressBar.setVisible(false);
		_dialog.setVisible(true);
	}


	/**
	 * Create dialog components
	 * @return Panel containing all gui elements in dialog
	 */
	private Component makeDialogComponents()
	{
		JPanel dialogPanel = new JPanel();
		dialogPanel.setLayout(new BorderLayout(0, 5));
		JPanel mainPanel = new JPanel();
		mainPanel.setLayout(new BoxLayout(mainPanel, BoxLayout.Y_AXIS));
		// Make a central panel with the text box and checkboxes
		JPanel descPanel = new JPanel();
		descPanel.setLayout(new FlowLayout());
		descPanel.add(new JLabel(I18nManager.getText("dialog.exportkml.text")));
		_descriptionField = new JTextField(20);
		_descriptionField.addKeyListener(new DialogCloser(_dialog));
		descPanel.add(_descriptionField);
		descPanel.setAlignmentX(Component.CENTER_ALIGNMENT);
		mainPanel.add(descPanel);
		dialogPanel.add(mainPanel, BorderLayout.CENTER);
		// point type selection
		_pointTypeSelector = new PointTypeSelector();
		_pointTypeSelector.setAlignmentX(Component.CENTER_ALIGNMENT);
		mainPanel.add(_pointTypeSelector);
		// Colour definition
		Color trackColour = ColourUtils.colourFromHex(Config.getConfigString(Config.KEY_KML_TRACK_COLOUR));
		if (trackColour == null) {
			trackColour = DEFAULT_TRACK_COLOUR;
		}
		_colourPatch = new ColourPatch(trackColour);
		_colourPatch.addMouseListener(new MouseAdapter() {
			public void mouseClicked(MouseEvent e) {
				_colourChooser.showDialog(_colourPatch.getBackground());
				Color colour = _colourChooser.getChosenColour();
				if (colour != null) _colourPatch.setColour(colour);
			}
		});
		JPanel colourPanel = new JPanel();
		colourPanel.add(new JLabel(I18nManager.getText("dialog.exportkml.trackcolour")));
		colourPanel.add(_colourPatch);
		mainPanel.add(colourPanel);
		// Pair of radio buttons for standard/extended KML
		JRadioButton standardKmlRadio = new JRadioButton(I18nManager.getText("dialog.exportkml.standardkml"));
		_gxExtensionsRadio = new JRadioButton(I18nManager.getText("dialog.exportkml.extendedkml"));
		ButtonGroup bGroup = new ButtonGroup();
		bGroup.add(standardKmlRadio); bGroup.add(_gxExtensionsRadio);
		JPanel radioPanel = new JPanel();
		radioPanel.setLayout(new FlowLayout(FlowLayout.CENTER, 10, 1));
		radioPanel.add(standardKmlRadio);
		radioPanel.add(_gxExtensionsRadio);
		standardKmlRadio.setSelected(true);
		mainPanel.add(radioPanel);
		// Checkbox for altitude export
		_altitudesCheckbox = new JCheckBox(I18nManager.getText("dialog.exportkml.altitude"));
		_altitudesCheckbox.setHorizontalTextPosition(SwingConstants.LEFT);
		_altitudesCheckbox.setAlignmentX(Component.CENTER_ALIGNMENT);
		mainPanel.add(_altitudesCheckbox);

		// Checkboxes for kmz export and image export
		_kmzCheckbox = new JCheckBox(I18nManager.getText("dialog.exportkml.kmz"));
		_kmzCheckbox.setHorizontalTextPosition(SwingConstants.LEFT);
		_kmzCheckbox.setAlignmentX(Component.CENTER_ALIGNMENT);
		// enable image checkbox if kmz activated
		_kmzCheckbox.addActionListener(new ActionListener() {
			public void actionPerformed(ActionEvent e)
			{
				enableCheckboxes();
			}
		});
		mainPanel.add(_kmzCheckbox);
		_exportImagesCheckbox = new JCheckBox(I18nManager.getText("dialog.exportkml.exportimages"));
		_exportImagesCheckbox.setHorizontalTextPosition(SwingConstants.LEFT);
		_exportImagesCheckbox.setAlignmentX(Component.CENTER_ALIGNMENT);
		// enable image size fields if image checkbox changes
		_exportImagesCheckbox.addActionListener(new ActionListener() {
			public void actionPerformed(ActionEvent arg0) {
				enableImageSizeFields();
			}
		});
		mainPanel.add(_exportImagesCheckbox);
		// Panel for the image size
		JPanel imageSizePanel = new JPanel();
		imageSizePanel.setLayout(new FlowLayout(FlowLayout.CENTER));
		_imageSizeLabel = new JLabel(I18nManager.getText("dialog.exportkml.imagesize"));
		_imageSizeLabel.setAlignmentX(Component.RIGHT_ALIGNMENT);
		imageSizePanel.add(_imageSizeLabel);
		_imageSizeField = new WholeNumberField(4);
		imageSizePanel.add(_imageSizeField);
		mainPanel.add(imageSizePanel);

		mainPanel.add(Box.createVerticalStrut(10));
		_progressLabel = new JLabel("...");
		_progressLabel.setAlignmentX(Component.CENTER_ALIGNMENT);
		mainPanel.add(_progressLabel);
		_progressBar = new JProgressBar(0, 100);
		_progressBar.setVisible(false);
		_progressBar.setAlignmentX(Component.CENTER_ALIGNMENT);
		mainPanel.add(_progressBar);
		mainPanel.add(Box.createVerticalStrut(10));
		// button panel at bottom
		JPanel buttonPanel = new JPanel();
		buttonPanel.setLayout(new FlowLayout(FlowLayout.RIGHT));
		_okButton = new JButton(I18nManager.getText("button.ok"));
		ActionListener okListener = new ActionListener() {
			public void actionPerformed(ActionEvent e)
			{
				startExport();
			}
		};
		_okButton.addActionListener(okListener);
		_descriptionField.addActionListener(okListener);
		buttonPanel.add(_okButton);
		JButton cancelButton = new JButton(I18nManager.getText("button.cancel"));
		cancelButton.addActionListener(new ActionListener() {
			public void actionPerformed(ActionEvent e)
			{
				_cancelPressed = true;
				_dialog.dispose();
			}
		});
		buttonPanel.add(cancelButton);
		dialogPanel.add(buttonPanel, BorderLayout.SOUTH);
		return dialogPanel;
	}


	/**
	 * Enable the checkboxes according to data
	 */
	private void enableCheckboxes()
	{
		_pointTypeSelector.init(_trackInfo);
		boolean hasAltitudes = _track.hasData(Field.ALTITUDE);
		if (!hasAltitudes) {_altitudesCheckbox.setSelected(false);}
		boolean hasPhotos = _trackInfo.getPhotoList() != null && _trackInfo.getPhotoList().getNumPhotos() > 0;
		_exportImagesCheckbox.setSelected(hasPhotos && _kmzCheckbox.isSelected());
		_exportImagesCheckbox.setEnabled(hasPhotos && _kmzCheckbox.isSelected());
		enableImageSizeFields();
	}

	/**
	 * Enable and disable the image size fields according to the checkboxes
	 */
	private void enableImageSizeFields()
	{
		boolean exportImages = _exportImagesCheckbox.isEnabled() && _exportImagesCheckbox.isSelected();
		_imageSizeField.setEnabled(exportImages);
		_imageSizeLabel.setEnabled(exportImages);
	}


	/**
	 * @return true if using gx extensions for kml export
	 */
	private boolean useGxExtensions() {
		return _gxExtensionsRadio.isSelected();
	}
	/**
	 * Start the export process based on the input parameters
	 */
	private void startExport()
	{
		// OK pressed, now validate selection checkboxes
		if (!_pointTypeSelector.getAnythingSelected()) {
			JOptionPane.showMessageDialog(_parentFrame, I18nManager.getText("dialog.save.notypesselected"),
				I18nManager.getText("dialog.saveoptions.title"), JOptionPane.WARNING_MESSAGE);
			return;
		}
		// Choose output file
		if (_fileChooser == null)
		{
			_fileChooser = new JFileChooser();
			_fileChooser.setDialogType(JFileChooser.SAVE_DIALOG);
			_fileChooser.setFileFilter(new GenericFileFilter("filetype.kmlkmz", new String[] {"kml", "kmz"}));
			// 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));}
		}
		String requiredExtension = null, otherExtension = null;
		if (_kmzCheckbox.isSelected()) {
			requiredExtension = ".kmz"; otherExtension = ".kml";
		}
		else {
			requiredExtension = ".kml"; otherExtension = ".kmz";
		}
		_fileChooser.setAcceptAllFileFilterUsed(false);
		// Allow choose again if an existing file is selected
		boolean chooseAgain = false;
		do
		{
			chooseAgain = false;
			if (_fileChooser.showSaveDialog(_parentFrame) == JFileChooser.APPROVE_OPTION)
			{
				// OK pressed and file chosen
				File file = _fileChooser.getSelectedFile();
				if (file.getName().toLowerCase().endsWith(otherExtension))
				{
					String path = file.getAbsolutePath();
					file = new File(path.substring(0, path.length()-otherExtension.length()) + requiredExtension);
				}
				else if (!file.getName().toLowerCase().endsWith(requiredExtension))
				{
					file = new File(file.getAbsolutePath() + requiredExtension);
				}
				// Check if file exists and if necessary prompt for overwrite
				Object[] buttonTexts = {I18nManager.getText("button.overwrite"), I18nManager.getText("button.cancel")};
				if (!file.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)
				{
					// New file or overwrite confirmed, so initiate export in separate thread
					_exportFile = file;
					_cancelPressed = false;
					new Thread(this).start();
				}
				else
				{
					chooseAgain = true;
				}
			}
		} while (chooseAgain);
	}


	/**
	 * Run method for controlling separate thread for exporting
	 */
	public void run()
	{
		// Disable ok button to stop second go
		_okButton.setEnabled(false);
		_descriptionField.setEnabled(false);
		// Initialise progress indicators
		_progressLabel.setText(I18nManager.getText("confirm.running"));
		_progressBar.setVisible(true);
		_progressBar.setValue(0);
		boolean exportToKmz = _kmzCheckbox.isSelected();
		boolean exportImages = exportToKmz && _exportImagesCheckbox.isSelected();
		_progressBar.setMaximum(exportImages?getNumPhotosToExport():1);

		// Create array for image dimensions in case it's required
		_imageDimensions = new Dimension[_track.getNumPoints()];

		OutputStreamWriter writer = null;
		ZipOutputStream zipOutputStream = null;
		try
		{
			// Select writer according to whether kmz requested or not
			if (!_kmzCheckbox.isSelected())
			{
				// normal writing to file
				writer = new OutputStreamWriter(new FileOutputStream(_exportFile));
			}
			else
			{
				// kmz requested - need zip output stream
				zipOutputStream = new ZipOutputStream(new FileOutputStream(_exportFile));
				// Export images into zip file too if requested
				if (exportImages)
				{
					// Get entered value for image size, store in config
					int thumbSize = _imageSizeField.getValue();
					if (thumbSize < DEFAULT_THUMBNAIL_WIDTH) {thumbSize = DEFAULT_THUMBNAIL_WIDTH;}
					Config.setConfigInt(Config.KEY_KMZ_IMAGE_SIZE, thumbSize);

					// Create thumbnails of each photo in turn and add to zip as images/image<n>.jpg
					// This is done first so that photo sizes are known for later
					exportThumbnails(zipOutputStream, thumbSize);
				}
				writer = new OutputStreamWriter(zipOutputStream);
				// Make an entry in the zip file for the kml file
				ZipEntry kmlEntry = new ZipEntry(KML_FILENAME_IN_KMZ);
				zipOutputStream.putNextEntry(kmlEntry);
			}
			// write file
			final int numPoints = exportData(writer, exportImages);
			// update config with selected track colour
			Config.setConfigString(Config.KEY_KML_TRACK_COLOUR, ColourUtils.makeHexCode(_colourPatch.getBackground()));
			// update progress bar
			_progressBar.setValue(1);

			// close zip entry if necessary
			if (zipOutputStream != null)
			{
				// Make sure all buffered data in writer is flushed
				writer.flush();
				// Close off this entry in the zip file
				zipOutputStream.closeEntry();
			}

			// close file
			writer.close();
			_imageDimensions = null;
			// Store directory in config for later
			Config.setConfigString(Config.KEY_TRACK_DIR, _exportFile.getParentFile().getAbsolutePath());
			// Add to recent file list
			Config.getRecentFileList().addFile(new RecentFile(_exportFile, true));
			// show confirmation
			UpdateMessageBroker.informSubscribers();
			UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.save.ok1")
				 + " " + numPoints + " " + I18nManager.getText("confirm.save.ok2")
				 + " " + _exportFile.getAbsolutePath());
			// export successful so need to close dialog and return
			_dialog.dispose();
			return;
		}
		catch (IOException ioe)
		{
			try {
				if (writer != null) writer.close();
			}
			catch (IOException ioe2) {}
			JOptionPane.showMessageDialog(_parentFrame,
				I18nManager.getText("error.save.failed") + " : " + ioe.getMessage(),
				I18nManager.getText("error.save.dialogtitle"), JOptionPane.ERROR_MESSAGE);
		}
		// if not returned already, export failed so need to recall the file selection
		startExport();
	}


	/**
	 * Export the information to the given writer
	 * @param inWriter writer object
	 * @param inExportImages true if image thumbnails are to be referenced
	 * @return number of points written
	 */
	private int exportData(OutputStreamWriter inWriter, boolean inExportImages)
	throws IOException
	{
		boolean writeTrack = _pointTypeSelector.getTrackpointsSelected();
		boolean writeWaypoints = _pointTypeSelector.getWaypointsSelected();
		boolean writePhotos = _pointTypeSelector.getPhotopointsSelected();
		boolean writeAudios = _pointTypeSelector.getAudiopointsSelected();
		boolean justSelection = _pointTypeSelector.getJustSelection();
		// Define xml header (depending on whether extensions are used or not)
		if (useGxExtensions()) {
			inWriter.write("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<kml xmlns=\"http://earth.google.com/kml/2.2\" xmlns:gx=\"http://www.google.com/kml/ext/2.2\">\n");
		}
		else {
			inWriter.write("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<kml xmlns=\"http://earth.google.com/kml/2.1\">\n");
		}
		inWriter.write("<Folder>\n\t<name>");
		if (_descriptionField != null && _descriptionField.getText() != null && !_descriptionField.getText().equals(""))
		{
			inWriter.write(_descriptionField.getText());
		}
		else {
			inWriter.write("Export from GpsPrune");
		}
		inWriter.write("</name>\n");

		// Examine selection if required
		int selStart = -1, selEnd = -1;
		if (justSelection) {
			selStart = _trackInfo.getSelection().getStart();
			selEnd = _trackInfo.getSelection().getEnd();
		}

		boolean absoluteAltitudes = _altitudesCheckbox.isSelected();
		int i = 0;
		DataPoint point = null;
		boolean hasTrackpoints = false;
		boolean writtenPhotoHeader = false, writtenAudioHeader = false;
		final int numPoints = _track.getNumPoints();
		int numSaved = 0;
		int photoNum = 0;
		// Loop over waypoints
		for (i=0; i<numPoints; i++)
		{
			point = _track.getPoint(i);
			boolean writeCurrentPoint = !justSelection || (i>=selStart && i<=selEnd);
			// Make a blob for each waypoint
			if (point.isWaypoint())
			{
				if (writeWaypoints && writeCurrentPoint)
				{
					exportWaypoint(point, inWriter, absoluteAltitudes);
					numSaved++;
				}
			}
			else if (!point.hasMedia())
			{
				hasTrackpoints = true;
			}
			// Make a blob with description for each photo
			// Photos have already been written so picture sizes already known
			if (point.getPhoto() != null && point.getPhoto().isValid() && writePhotos && writeCurrentPoint)
			{
				if (!writtenPhotoHeader)
				{
					inWriter.write("<Style id=\"camera_icon\"><IconStyle><Icon><href>http://maps.google.com/mapfiles/kml/pal4/icon46.png</href></Icon></IconStyle></Style>");
					writtenPhotoHeader = true;
				}
				photoNum++;
				exportPhotoPoint(point, inWriter, inExportImages, i, photoNum, absoluteAltitudes);
				numSaved++;
			}
			// Make a blob with description for each audio clip
			if (point.getAudio() != null && writeAudios && writeCurrentPoint)
			{
				if (!writtenAudioHeader)
				{
					inWriter.write("<Style id=\"audio_icon\"><IconStyle><color>ff00ffff</color><Icon><href>http://maps.google.com/mapfiles/kml/shapes/star.png</href></Icon></IconStyle></Style>");
					writtenAudioHeader = true;
				}
				exportAudioPoint(point, inWriter, absoluteAltitudes);
				numSaved++;
			}
		}
		// Make a line for the track, if there is one
		if (hasTrackpoints && writeTrack)
		{
			boolean useGxExtensions = _gxExtensionsRadio.isSelected();
			if (useGxExtensions)
			{
				// Write track using the Google Extensions to KML including gx:Track
				numSaved += writeGxTrack(inWriter, absoluteAltitudes, selStart, selEnd);
			}
			else {
				// Write track using standard KML
				numSaved += writeStandardTrack(inWriter, absoluteAltitudes, selStart, selEnd);
			}
		}
		inWriter.write("</Folder>\n</kml>");
		return numSaved;
	}


	/**
	 * Write out the track using standard KML LineString tag
	 * @param inWriter writer object to write to
	 * @param inAbsoluteAltitudes true to use absolute altitudes, false to clamp to ground
	 * @param inSelStart start index of selection, or -1 if whole track
	 * @param inSelEnd     end index of selection, or -1 if whole track
	 * @return number of track points written
	 */
	private int writeStandardTrack(OutputStreamWriter inWriter, boolean inAbsoluteAltitudes, int inSelStart,
		int inSelEnd)
	throws IOException
	{
		int numSaved = 0;
		// Set up strings for start and end of track segment
		String trackStart = "\t<Placemark>\n\t\t<name>track</name>\n\t\t<Style>\n\t\t\t<LineStyle>\n"
			+ "\t\t\t\t<color>cc" + reverse(ColourUtils.makeHexCode(_colourPatch.getBackground())) + "</color>\n"
			+ "\t\t\t\t<width>4</width>\n\t\t\t</LineStyle>\n"
			+ "\t\t</Style>\n\t\t<LineString>\n";
		if (inAbsoluteAltitudes) {
			trackStart += "\t\t\t<extrude>1</extrude>\n\t\t\t<altitudeMode>absolute</altitudeMode>\n";
		}
		else {
			trackStart += "\t\t\t<altitudeMode>clampToGround</altitudeMode>\n";
		}
		trackStart += "\t\t\t<coordinates>";
		String trackEnd = "\t\t\t</coordinates>\n\t\t</LineString>\n\t</Placemark>";

		boolean justSelection = _pointTypeSelector.getJustSelection();

		// Start segment
		inWriter.write(trackStart);
		// Loop over track points
		boolean firstTrackpoint = true;
		final int numPoints = _track.getNumPoints();
		for (int i=0; i<numPoints; i++)
		{
			DataPoint point = _track.getPoint(i);
			boolean writeCurrentPoint = !justSelection || (i>=inSelStart && i<=inSelEnd);
			if (!point.isWaypoint() && writeCurrentPoint)
			{
				// start new track segment if necessary
				if (point.getSegmentStart() && !firstTrackpoint) {
					inWriter.write(trackEnd);
					inWriter.write(trackStart);
				}
				if (point.getPhoto() == null)
				{
					exportTrackpoint(point, inWriter);
					numSaved++;
					firstTrackpoint = false;
				}
			}
		}
		// end segment
		inWriter.write(trackEnd);
		return numSaved;
	}


	/**
	 * Write out the track using Google's KML Extensions such as gx:Track
	 * @param inWriter writer object to write to
	 * @param inAbsoluteAltitudes true to use absolute altitudes, false to clamp to ground
	 * @param inSelStart start index of selection, or -1 if whole track
	 * @param inSelEnd     end index of selection, or -1 if whole track
	 * @return number of track points written
	 */
	private int writeGxTrack(OutputStreamWriter inWriter, boolean inAbsoluteAltitudes, int inSelStart,
		int inSelEnd)
	throws IOException
	{
		int numSaved = 0;
		// Set up strings for start and end of track segment
		String trackStart = "\t<Placemark>\n\t\t<name>track</name>\n\t\t<Style>\n\t\t\t<LineStyle>\n"
			+ "\t\t\t\t<color>cc" + reverse(ColourUtils.makeHexCode(_colourPatch.getBackground())) + "</color>\n"
			+ "\t\t\t\t<width>4</width>\n\t\t\t</LineStyle>\n"
			+ "\t\t</Style>\n\t\t<gx:Track>\n";
		if (inAbsoluteAltitudes) {
			trackStart += "\t\t\t<extrude>1</extrude>\n\t\t\t<altitudeMode>absolute</altitudeMode>\n";
		}
		else {
			trackStart += "\t\t\t<altitudeMode>clampToGround</altitudeMode>\n";
		}
		String trackEnd = "\n\t\t</gx:Track>\n\t</Placemark>\n";

		boolean justSelection = _pointTypeSelector.getJustSelection();

		// Start segment
		inWriter.write(trackStart);
		StringBuilder whenList = new StringBuilder();
		StringBuilder coordList = new StringBuilder();

		// Loop over track points
		boolean firstTrackpoint = true;
		final int numPoints = _track.getNumPoints();
		for (int i=0; i<numPoints; i++)
		{
			DataPoint point = _track.getPoint(i);
			boolean writeCurrentPoint = !justSelection || (i>=inSelStart && i<=inSelEnd);
			if (!point.isWaypoint() && writeCurrentPoint)
			{
				// start new track segment if necessary
				if (point.getSegmentStart() && !firstTrackpoint)
				{
					inWriter.write(whenList.toString());
					inWriter.write('\n');
					inWriter.write(coordList.toString());
					inWriter.write('\n');
					inWriter.write(trackEnd);
					whenList.setLength(0); coordList.setLength(0);
					inWriter.write(trackStart);
				}
				if (point.getPhoto() == null)
				{
					// Add timestamp (if any) to the list
					whenList.append("<when>");
					if (point.hasTimestamp()) {
						whenList.append(point.getTimestamp().getText(Timestamp.Format.ISO8601));
					}
					whenList.append("</when>\n");
					// Add coordinates to the list
					coordList.append("<gx:coord>");
					coordList.append(point.getLongitude().output(Coordinate.FORMAT_DECIMAL_FORCE_POINT)).append(' ');
					coordList.append(point.getLatitude().output(Coordinate.FORMAT_DECIMAL_FORCE_POINT)).append(' ');
					if (point.hasAltitude()) {
						coordList.append("" + point.getAltitude().getStringValue(UnitSetLibrary.UNITS_METRES));
					}
					else {
						coordList.append('0');
					}
					coordList.append("</gx:coord>\n");
					numSaved++;
					firstTrackpoint = false;
				}
			}
		}
		// end segment
		inWriter.write(whenList.toString());
		inWriter.write('\n');
		inWriter.write(coordList.toString());
		inWriter.write('\n');
		inWriter.write(trackEnd);
		return numSaved;
	}


	/**
	 * Reverse the hex code for the colours for KML's stupid backwards format
	 * @param inCode colour code rrggbb
	 * @return kml code bbggrr
	 */
	private static String reverse(String inCode)
	{
		return inCode.substring(4, 6) + inCode.substring(2, 4) + inCode.substring(0, 2);
	}

	/**
	 * Export the specified waypoint into the file
	 * @param inPoint waypoint to export
	 * @param inWriter writer object
	 * @param inAbsoluteAltitude true for absolute altitude
	 * @throws IOException on write failure
	 */
	private void exportWaypoint(DataPoint inPoint, Writer inWriter, boolean inAbsoluteAltitude) throws IOException
	{
		String name = inPoint.getWaypointName().trim();
		exportNamedPoint(inPoint, inWriter, name, inPoint.getFieldValue(Field.DESCRIPTION), null, inAbsoluteAltitude);
	}


	/**
	 * Export the specified audio point into the file
	 * @param inPoint audio point to export
	 * @param inWriter writer object
	 * @param inAbsoluteAltitude true for absolute altitude
	 * @throws IOException on write failure
	 */
	private void exportAudioPoint(DataPoint inPoint, Writer inWriter, boolean inAbsoluteAltitude) throws IOException
	{
		String name = inPoint.getAudio().getName();
		String desc = null;
		if (inPoint.getAudio().getFile() != null) {
			desc = inPoint.getAudio().getFile().getAbsolutePath();
		}
		exportNamedPoint(inPoint, inWriter, name, desc, "audio_icon", inAbsoluteAltitude);
	}


	/**
	 * Export the specified photo into the file
	 * @param inPoint data point including photo
	 * @param inWriter writer object
	 * @param inImageLink flag to set whether to export image links or not
	 * @param inPointNumber number of point for accessing dimensions
	 * @param inImageNumber number of image for filename
	 * @param inAbsoluteAltitude true for absolute altitudes
	 * @throws IOException on write failure
	 */
	private void exportPhotoPoint(DataPoint inPoint, Writer inWriter, boolean inImageLink,
		int inPointNumber, int inImageNumber, boolean inAbsoluteAltitude)
	throws IOException
	{
		String name = inPoint.getPhoto().getName();
		String desc = null;
		if (inImageLink)
		{
			Dimension imageSize = _imageDimensions[inPointNumber];
			// Create html for the thumbnail images
			desc = "<![CDATA[<br/><table border='0'><tr><td><center><img src='images/image"
				+ inImageNumber + ".jpg' width='" + imageSize.width + "' height='" + imageSize.height + "'></center></td></tr>"
				+ "<tr><td><center>" + name + "</center></td></tr></table>]]>";
		}
		// Export point
		exportNamedPoint(inPoint, inWriter, name, desc, "camera_icon", inAbsoluteAltitude);
	}


	/**
	 * Export the specified named point into the file, like waypoint or photo point
	 * @param inPoint data point
	 * @param inWriter writer object
	 * @param inName name of point
	 * @param inDesc description of point, or null
	 * @param inStyle style of point, or null
	 * @param inAbsoluteAltitude true for absolute altitudes
	 * @throws IOException on write failure
	 */
	private void exportNamedPoint(DataPoint inPoint, Writer inWriter, String inName,
		String inDesc, String inStyle, boolean inAbsoluteAltitude)
	throws IOException
	{
		inWriter.write("\t<Placemark>\n\t\t<name>");
		inWriter.write(inName);
		inWriter.write("</name>\n");
		if (inDesc != null)
		{
			// Write out description
			inWriter.write("\t\t<description>");
			inWriter.write(XmlUtils.fixCdata(inDesc));
			inWriter.write("</description>\n");
		}
		if (inStyle != null)
		{
			inWriter.write("<styleUrl>#");
			inWriter.write(inStyle);
			inWriter.write("</styleUrl>\n");
		}
		inWriter.write("\t\t<Point>\n");
		if (inAbsoluteAltitude && inPoint.hasAltitude()) {
			inWriter.write("\t\t\t<altitudeMode>absolute</altitudeMode>\n");
		}
		else {
			inWriter.write("\t\t\t<altitudeMode>clampToGround</altitudeMode>\n");
		}
		inWriter.write("\t\t\t<coordinates>");
		inWriter.write(inPoint.getLongitude().output(Coordinate.FORMAT_DECIMAL_FORCE_POINT));
		inWriter.write(',');
		inWriter.write(inPoint.getLatitude().output(Coordinate.FORMAT_DECIMAL_FORCE_POINT));
		inWriter.write(',');
		// Altitude if point has one
		if (inPoint.hasAltitude()) {
			inWriter.write("" + inPoint.getAltitude().getStringValue(UnitSetLibrary.UNITS_METRES));
		}
		else {
			inWriter.write('0');
		}
		inWriter.write("</coordinates>\n\t\t</Point>\n\t</Placemark>\n");
	}


	/**
	 * Export the specified trackpoint into the file
	 * @param inPoint trackpoint to export
	 * @param inWriter writer object
	 */
	private void exportTrackpoint(DataPoint inPoint, Writer inWriter) throws IOException
	{
		inWriter.write(inPoint.getLongitude().output(Coordinate.FORMAT_DECIMAL_FORCE_POINT));
		inWriter.write(',');
		inWriter.write(inPoint.getLatitude().output(Coordinate.FORMAT_DECIMAL_FORCE_POINT));
		// Altitude if point has one
		inWriter.write(',');
		if (inPoint.hasAltitude()) {
			inWriter.write("" + inPoint.getAltitude().getStringValue(UnitSetLibrary.UNITS_METRES));
		}
		else {
			inWriter.write('0');
		}
		inWriter.write('\n');
	}


	/**
	 * Loop through the photos and create thumbnails
	 * @param inZipStream zip stream to save image files to
	 * @param inThumbSize thumbnail size
	 */
	private void exportThumbnails(ZipOutputStream inZipStream, int inThumbSize)
	throws IOException
	{
		// set up image writer
		Iterator<ImageWriter> writers = ImageIO.getImageWritersByFormatName("jpg");
		if (writers == null || !writers.hasNext())
		{
			throw new IOException("no JPEG writer found");
		}
		ImageWriter imageWriter = writers.next();

		// Check selection checkbox
		final boolean justSelection = _pointTypeSelector.getJustSelection();
		int selStart = -1, selEnd = -1;
		if (justSelection) {
			selStart = _trackInfo.getSelection().getStart();
			selEnd = _trackInfo.getSelection().getEnd();
		}

		final int numPoints = _track.getNumPoints();
		DataPoint point = null;
		int photoNum = 0;
		// Loop over all points in track
		for (int i=0; i<numPoints && !_cancelPressed; i++)
		{
			point = _track.getPoint(i);
			if (point.getPhoto() != null && point.getPhoto().isValid() && (!justSelection || (i>=selStart && i<=selEnd)))
			{
				photoNum++;
				// Make a new entry in zip file
				ZipEntry entry = new ZipEntry("images/image" + photoNum + ".jpg");
				inZipStream.putNextEntry(entry);
				// Load image and write to outstream
				ImageIcon icon = point.getPhoto().createImageIcon();

				// Scale image to required size (not smoothed)
				BufferedImage bufferedImage = ImageUtils.rotateImage(icon.getImage(),
					inThumbSize, inThumbSize, point.getPhoto().getRotationDegrees());
				// Store image dimensions so that it doesn't have to be calculated again for the points
				_imageDimensions[i] = new Dimension(bufferedImage.getWidth(), bufferedImage.getHeight());

				imageWriter.setOutput(ImageIO.createImageOutputStream(inZipStream));
				imageWriter.write(bufferedImage);
				// Close zip file entry
				inZipStream.closeEntry();
				// Update progress bar
				_progressBar.setValue(photoNum+1);
			}
		}
	}


	/**
	 * @return number of correlated photos in the track
	 */
	private int getNumPhotosToExport()
	{
		int numPoints = _track.getNumPoints();
		int numPhotos = 0;
		DataPoint point = null;
		// Loop over all points in track
		for (int i=0; i<numPoints; i++)
		{
			point = _track.getPoint(i);
			if (point.getPhoto() != null) {
				numPhotos++;
			}
		}
		return numPhotos;
	}
}
