1 package tim.prune.save;
3 import java.awt.BorderLayout;
5 import java.awt.Component;
6 import java.awt.Dimension;
7 import java.awt.FlowLayout;
8 import java.awt.event.ActionEvent;
9 import java.awt.event.ActionListener;
10 import java.awt.event.MouseAdapter;
11 import java.awt.event.MouseEvent;
12 import java.awt.image.BufferedImage;
14 import java.io.FileOutputStream;
15 import java.io.IOException;
16 import java.io.OutputStreamWriter;
17 import java.io.Writer;
18 import java.util.Iterator;
19 import java.util.zip.ZipEntry;
20 import java.util.zip.ZipOutputStream;
22 import javax.imageio.ImageIO;
23 import javax.imageio.ImageWriter;
24 import javax.swing.Box;
25 import javax.swing.BoxLayout;
26 import javax.swing.ButtonGroup;
27 import javax.swing.ImageIcon;
28 import javax.swing.JButton;
29 import javax.swing.JCheckBox;
30 import javax.swing.JDialog;
31 import javax.swing.JFileChooser;
32 import javax.swing.JLabel;
33 import javax.swing.JOptionPane;
34 import javax.swing.JPanel;
35 import javax.swing.JProgressBar;
36 import javax.swing.JRadioButton;
37 import javax.swing.JTextField;
38 import javax.swing.SwingConstants;
41 import tim.prune.GenericFunction;
42 import tim.prune.I18nManager;
43 import tim.prune.UpdateMessageBroker;
44 import tim.prune.config.ColourUtils;
45 import tim.prune.config.Config;
46 import tim.prune.data.Coordinate;
47 import tim.prune.data.DataPoint;
48 import tim.prune.data.Field;
49 import tim.prune.data.RecentFile;
50 import tim.prune.data.Timestamp;
51 import tim.prune.data.Track;
52 import tim.prune.data.TrackInfo;
53 import tim.prune.data.UnitSetLibrary;
54 import tim.prune.gui.DialogCloser;
55 import tim.prune.gui.ImageUtils;
56 import tim.prune.gui.WholeNumberField;
57 import tim.prune.gui.colour.ColourChooser;
58 import tim.prune.gui.colour.ColourPatch;
59 import tim.prune.load.GenericFileFilter;
60 import tim.prune.save.xml.XmlUtils;
63 * Class to export track information
64 * into a specified Kml or Kmz file
66 public class KmlExporter extends GenericFunction implements Runnable
68 private TrackInfo _trackInfo = null;
69 private Track _track = null;
70 private JDialog _dialog = null;
71 private JTextField _descriptionField = null;
72 private PointTypeSelector _pointTypeSelector = null;
73 private JRadioButton _gxExtensionsRadio = null;
74 private JCheckBox _altitudesCheckbox = null;
75 private JCheckBox _kmzCheckbox = null;
76 private JCheckBox _exportImagesCheckbox = null;
77 private JLabel _imageSizeLabel = null;
78 private WholeNumberField _imageSizeField = null;
79 private ColourPatch _colourPatch = null;
80 private JLabel _progressLabel = null;
81 private JProgressBar _progressBar = null;
82 private Dimension[] _imageDimensions = null;
83 private JFileChooser _fileChooser = null;
84 private File _exportFile = null;
85 private JButton _okButton = null;
86 private boolean _cancelPressed = false;
87 private ColourChooser _colourChooser = null;
89 // Filename of Kml file within zip archive
90 private static final String KML_FILENAME_IN_KMZ = "doc.kml";
91 // Default width and height of thumbnail images in Kmz
92 private static final int DEFAULT_THUMBNAIL_WIDTH = 240;
93 // Default track colour
94 private static final Color DEFAULT_TRACK_COLOUR = new Color(204, 0, 0); // red
99 * @param inApp app object
101 public KmlExporter(App inApp)
104 _trackInfo = inApp.getTrackInfo();
105 _track = _trackInfo.getTrack();
109 public String getNameKey() {
110 return "function.exportkml";
114 * Show the dialog to select options and export file
118 // Make dialog window including whether to compress to kmz (and include pictures) or not
121 _dialog = new JDialog(_parentFrame, I18nManager.getText(getNameKey()), true);
122 _dialog.setLocationRelativeTo(_parentFrame);
123 _dialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE);
124 _dialog.getContentPane().add(makeDialogComponents());
126 _colourChooser = new ColourChooser(_dialog);
128 // Fill in image size from config
129 _imageSizeField.setValue(Config.getConfigInt(Config.KEY_KMZ_IMAGE_SIZE));
131 _descriptionField.setEnabled(true);
132 _okButton.setEnabled(true);
133 _progressLabel.setText("");
134 _progressBar.setVisible(false);
135 _dialog.setVisible(true);
140 * Create dialog components
141 * @return Panel containing all gui elements in dialog
143 private Component makeDialogComponents()
145 JPanel dialogPanel = new JPanel();
146 dialogPanel.setLayout(new BorderLayout(0, 5));
147 JPanel mainPanel = new JPanel();
148 mainPanel.setLayout(new BoxLayout(mainPanel, BoxLayout.Y_AXIS));
149 // Make a central panel with the text box and checkboxes
150 JPanel descPanel = new JPanel();
151 descPanel.setLayout(new FlowLayout());
152 descPanel.add(new JLabel(I18nManager.getText("dialog.exportkml.text")));
153 _descriptionField = new JTextField(20);
154 _descriptionField.addKeyListener(new DialogCloser(_dialog));
155 descPanel.add(_descriptionField);
156 descPanel.setAlignmentX(Component.CENTER_ALIGNMENT);
157 mainPanel.add(descPanel);
158 dialogPanel.add(mainPanel, BorderLayout.CENTER);
159 // point type selection
160 _pointTypeSelector = new PointTypeSelector();
161 _pointTypeSelector.setAlignmentX(Component.CENTER_ALIGNMENT);
162 mainPanel.add(_pointTypeSelector);
164 Color trackColour = ColourUtils.colourFromHex(Config.getConfigString(Config.KEY_KML_TRACK_COLOUR));
165 if (trackColour == null) {
166 trackColour = DEFAULT_TRACK_COLOUR;
168 _colourPatch = new ColourPatch(trackColour);
169 _colourPatch.addMouseListener(new MouseAdapter() {
170 public void mouseClicked(MouseEvent e) {
171 _colourChooser.showDialog(_colourPatch.getBackground());
172 Color colour = _colourChooser.getChosenColour();
173 if (colour != null) _colourPatch.setColour(colour);
176 JPanel colourPanel = new JPanel();
177 colourPanel.add(new JLabel(I18nManager.getText("dialog.exportkml.trackcolour")));
178 colourPanel.add(_colourPatch);
179 mainPanel.add(colourPanel);
180 // Pair of radio buttons for standard/extended KML
181 JRadioButton standardKmlRadio = new JRadioButton(I18nManager.getText("dialog.exportkml.standardkml"));
182 _gxExtensionsRadio = new JRadioButton(I18nManager.getText("dialog.exportkml.extendedkml"));
183 ButtonGroup bGroup = new ButtonGroup();
184 bGroup.add(standardKmlRadio); bGroup.add(_gxExtensionsRadio);
185 JPanel radioPanel = new JPanel();
186 radioPanel.setLayout(new FlowLayout(FlowLayout.CENTER, 10, 1));
187 radioPanel.add(standardKmlRadio);
188 radioPanel.add(_gxExtensionsRadio);
189 standardKmlRadio.setSelected(true);
190 mainPanel.add(radioPanel);
191 // Checkbox for altitude export
192 _altitudesCheckbox = new JCheckBox(I18nManager.getText("dialog.exportkml.altitude"));
193 _altitudesCheckbox.setHorizontalTextPosition(SwingConstants.LEFT);
194 _altitudesCheckbox.setAlignmentX(Component.CENTER_ALIGNMENT);
195 mainPanel.add(_altitudesCheckbox);
197 // Checkboxes for kmz export and image export
198 _kmzCheckbox = new JCheckBox(I18nManager.getText("dialog.exportkml.kmz"));
199 _kmzCheckbox.setHorizontalTextPosition(SwingConstants.LEFT);
200 _kmzCheckbox.setAlignmentX(Component.CENTER_ALIGNMENT);
201 // enable image checkbox if kmz activated
202 _kmzCheckbox.addActionListener(new ActionListener() {
203 public void actionPerformed(ActionEvent e)
208 mainPanel.add(_kmzCheckbox);
209 _exportImagesCheckbox = new JCheckBox(I18nManager.getText("dialog.exportkml.exportimages"));
210 _exportImagesCheckbox.setHorizontalTextPosition(SwingConstants.LEFT);
211 _exportImagesCheckbox.setAlignmentX(Component.CENTER_ALIGNMENT);
212 // enable image size fields if image checkbox changes
213 _exportImagesCheckbox.addActionListener(new ActionListener() {
214 public void actionPerformed(ActionEvent arg0) {
215 enableImageSizeFields();
218 mainPanel.add(_exportImagesCheckbox);
219 // Panel for the image size
220 JPanel imageSizePanel = new JPanel();
221 imageSizePanel.setLayout(new FlowLayout(FlowLayout.CENTER));
222 _imageSizeLabel = new JLabel(I18nManager.getText("dialog.exportkml.imagesize"));
223 _imageSizeLabel.setAlignmentX(Component.RIGHT_ALIGNMENT);
224 imageSizePanel.add(_imageSizeLabel);
225 _imageSizeField = new WholeNumberField(4);
226 imageSizePanel.add(_imageSizeField);
227 mainPanel.add(imageSizePanel);
229 mainPanel.add(Box.createVerticalStrut(10));
230 _progressLabel = new JLabel("...");
231 _progressLabel.setAlignmentX(Component.CENTER_ALIGNMENT);
232 mainPanel.add(_progressLabel);
233 _progressBar = new JProgressBar(0, 100);
234 _progressBar.setVisible(false);
235 _progressBar.setAlignmentX(Component.CENTER_ALIGNMENT);
236 mainPanel.add(_progressBar);
237 mainPanel.add(Box.createVerticalStrut(10));
238 // button panel at bottom
239 JPanel buttonPanel = new JPanel();
240 buttonPanel.setLayout(new FlowLayout(FlowLayout.RIGHT));
241 _okButton = new JButton(I18nManager.getText("button.ok"));
242 ActionListener okListener = new ActionListener() {
243 public void actionPerformed(ActionEvent e)
248 _okButton.addActionListener(okListener);
249 _descriptionField.addActionListener(okListener);
250 buttonPanel.add(_okButton);
251 JButton cancelButton = new JButton(I18nManager.getText("button.cancel"));
252 cancelButton.addActionListener(new ActionListener() {
253 public void actionPerformed(ActionEvent e)
255 _cancelPressed = true;
259 buttonPanel.add(cancelButton);
260 dialogPanel.add(buttonPanel, BorderLayout.SOUTH);
266 * Enable the checkboxes according to data
268 private void enableCheckboxes()
270 _pointTypeSelector.init(_trackInfo);
271 boolean hasAltitudes = _track.hasData(Field.ALTITUDE);
272 if (!hasAltitudes) {_altitudesCheckbox.setSelected(false);}
273 boolean hasPhotos = _trackInfo.getPhotoList() != null && _trackInfo.getPhotoList().getNumPhotos() > 0;
274 _exportImagesCheckbox.setSelected(hasPhotos && _kmzCheckbox.isSelected());
275 _exportImagesCheckbox.setEnabled(hasPhotos && _kmzCheckbox.isSelected());
276 enableImageSizeFields();
280 * Enable and disable the image size fields according to the checkboxes
282 private void enableImageSizeFields()
284 boolean exportImages = _exportImagesCheckbox.isEnabled() && _exportImagesCheckbox.isSelected();
285 _imageSizeField.setEnabled(exportImages);
286 _imageSizeLabel.setEnabled(exportImages);
291 * @return true if using gx extensions for kml export
293 private boolean useGxExtensions() {
294 return _gxExtensionsRadio.isSelected();
297 * Start the export process based on the input parameters
299 private void startExport()
301 // OK pressed, now validate selection checkboxes
302 if (!_pointTypeSelector.getAnythingSelected()) {
303 JOptionPane.showMessageDialog(_parentFrame, I18nManager.getText("dialog.save.notypesselected"),
304 I18nManager.getText("dialog.saveoptions.title"), JOptionPane.WARNING_MESSAGE);
307 // Choose output file
308 if (_fileChooser == null)
310 _fileChooser = new JFileChooser();
311 _fileChooser.setDialogType(JFileChooser.SAVE_DIALOG);
312 _fileChooser.setFileFilter(new GenericFileFilter("filetype.kmlkmz", new String[] {"kml", "kmz"}));
313 // start from directory in config which should be set
314 String configDir = Config.getConfigString(Config.KEY_TRACK_DIR);
315 if (configDir != null) {_fileChooser.setCurrentDirectory(new File(configDir));}
317 String requiredExtension = null, otherExtension = null;
318 if (_kmzCheckbox.isSelected()) {
319 requiredExtension = ".kmz"; otherExtension = ".kml";
322 requiredExtension = ".kml"; otherExtension = ".kmz";
324 _fileChooser.setAcceptAllFileFilterUsed(false);
325 // Allow choose again if an existing file is selected
326 boolean chooseAgain = false;
330 if (_fileChooser.showSaveDialog(_parentFrame) == JFileChooser.APPROVE_OPTION)
332 // OK pressed and file chosen
333 File file = _fileChooser.getSelectedFile();
334 if (file.getName().toLowerCase().endsWith(otherExtension))
336 String path = file.getAbsolutePath();
337 file = new File(path.substring(0, path.length()-otherExtension.length()) + requiredExtension);
339 else if (!file.getName().toLowerCase().endsWith(requiredExtension))
341 file = new File(file.getAbsolutePath() + requiredExtension);
343 // Check if file exists and if necessary prompt for overwrite
344 Object[] buttonTexts = {I18nManager.getText("button.overwrite"), I18nManager.getText("button.cancel")};
345 if (!file.exists() || JOptionPane.showOptionDialog(_parentFrame,
346 I18nManager.getText("dialog.save.overwrite.text"),
347 I18nManager.getText("dialog.save.overwrite.title"), JOptionPane.YES_NO_OPTION,
348 JOptionPane.WARNING_MESSAGE, null, buttonTexts, buttonTexts[1])
349 == JOptionPane.YES_OPTION)
351 // New file or overwrite confirmed, so initiate export in separate thread
353 _cancelPressed = false;
354 new Thread(this).start();
361 } while (chooseAgain);
366 * Run method for controlling separate thread for exporting
370 // Disable ok button to stop second go
371 _okButton.setEnabled(false);
372 _descriptionField.setEnabled(false);
373 // Initialise progress indicators
374 _progressLabel.setText(I18nManager.getText("confirm.running"));
375 _progressBar.setVisible(true);
376 _progressBar.setValue(0);
377 boolean exportToKmz = _kmzCheckbox.isSelected();
378 boolean exportImages = exportToKmz && _exportImagesCheckbox.isSelected();
379 _progressBar.setMaximum(exportImages?getNumPhotosToExport():1);
381 // Create array for image dimensions in case it's required
382 _imageDimensions = new Dimension[_track.getNumPoints()];
384 OutputStreamWriter writer = null;
385 ZipOutputStream zipOutputStream = null;
388 // Select writer according to whether kmz requested or not
389 if (!_kmzCheckbox.isSelected())
391 // normal writing to file
392 writer = new OutputStreamWriter(new FileOutputStream(_exportFile));
396 // kmz requested - need zip output stream
397 zipOutputStream = new ZipOutputStream(new FileOutputStream(_exportFile));
398 // Export images into zip file too if requested
401 // Get entered value for image size, store in config
402 int thumbSize = _imageSizeField.getValue();
403 if (thumbSize < DEFAULT_THUMBNAIL_WIDTH) {thumbSize = DEFAULT_THUMBNAIL_WIDTH;}
404 Config.setConfigInt(Config.KEY_KMZ_IMAGE_SIZE, thumbSize);
406 // Create thumbnails of each photo in turn and add to zip as images/image<n>.jpg
407 // This is done first so that photo sizes are known for later
408 exportThumbnails(zipOutputStream, thumbSize);
410 writer = new OutputStreamWriter(zipOutputStream);
411 // Make an entry in the zip file for the kml file
412 ZipEntry kmlEntry = new ZipEntry(KML_FILENAME_IN_KMZ);
413 zipOutputStream.putNextEntry(kmlEntry);
416 final int numPoints = exportData(writer, exportImages);
417 // update config with selected track colour
418 Config.setConfigString(Config.KEY_KML_TRACK_COLOUR, ColourUtils.makeHexCode(_colourPatch.getBackground()));
419 // update progress bar
420 _progressBar.setValue(1);
422 // close zip entry if necessary
423 if (zipOutputStream != null)
425 // Make sure all buffered data in writer is flushed
427 // Close off this entry in the zip file
428 zipOutputStream.closeEntry();
433 _imageDimensions = null;
434 // Store directory in config for later
435 Config.setConfigString(Config.KEY_TRACK_DIR, _exportFile.getParentFile().getAbsolutePath());
436 // Add to recent file list
437 Config.getRecentFileList().addFile(new RecentFile(_exportFile, true));
439 UpdateMessageBroker.informSubscribers();
440 UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.save.ok1")
441 + " " + numPoints + " " + I18nManager.getText("confirm.save.ok2")
442 + " " + _exportFile.getAbsolutePath());
443 // export successful so need to close dialog and return
447 catch (IOException ioe)
450 if (writer != null) writer.close();
452 catch (IOException ioe2) {}
453 JOptionPane.showMessageDialog(_parentFrame,
454 I18nManager.getText("error.save.failed") + " : " + ioe.getMessage(),
455 I18nManager.getText("error.save.dialogtitle"), JOptionPane.ERROR_MESSAGE);
457 // if not returned already, export failed so need to recall the file selection
463 * Export the information to the given writer
464 * @param inWriter writer object
465 * @param inExportImages true if image thumbnails are to be referenced
466 * @return number of points written
468 private int exportData(OutputStreamWriter inWriter, boolean inExportImages)
471 boolean writeTrack = _pointTypeSelector.getTrackpointsSelected();
472 boolean writeWaypoints = _pointTypeSelector.getWaypointsSelected();
473 boolean writePhotos = _pointTypeSelector.getPhotopointsSelected();
474 boolean writeAudios = _pointTypeSelector.getAudiopointsSelected();
475 boolean justSelection = _pointTypeSelector.getJustSelection();
476 // Define xml header (depending on whether extensions are used or not)
477 if (useGxExtensions()) {
478 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");
481 inWriter.write("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<kml xmlns=\"http://earth.google.com/kml/2.1\">\n");
483 inWriter.write("<Folder>\n\t<name>");
484 if (_descriptionField != null && _descriptionField.getText() != null && !_descriptionField.getText().equals(""))
486 inWriter.write(XmlUtils.fixCdata(_descriptionField.getText()));
489 inWriter.write("Export from GpsPrune");
491 inWriter.write("</name>\n");
493 // Examine selection if required
494 int selStart = -1, selEnd = -1;
496 selStart = _trackInfo.getSelection().getStart();
497 selEnd = _trackInfo.getSelection().getEnd();
500 boolean absoluteAltitudes = _altitudesCheckbox.isSelected();
502 DataPoint point = null;
503 boolean hasTrackpoints = false;
504 boolean writtenPhotoHeader = false, writtenAudioHeader = false;
505 final int numPoints = _track.getNumPoints();
508 // Loop over waypoints
509 for (i=0; i<numPoints; i++)
511 point = _track.getPoint(i);
512 boolean writeCurrentPoint = !justSelection || (i>=selStart && i<=selEnd);
513 // Make a blob for each waypoint
514 if (point.isWaypoint())
516 if (writeWaypoints && writeCurrentPoint)
518 exportWaypoint(point, inWriter, absoluteAltitudes);
522 else if (!point.hasMedia())
524 hasTrackpoints = true;
526 // Make a blob with description for each photo
527 // Photos have already been written so picture sizes already known
528 if (point.getPhoto() != null && point.getPhoto().isValid() && writePhotos && writeCurrentPoint)
530 if (!writtenPhotoHeader)
532 inWriter.write("<Style id=\"camera_icon\"><IconStyle><Icon><href>http://maps.google.com/mapfiles/kml/pal4/icon46.png</href></Icon></IconStyle></Style>");
533 writtenPhotoHeader = true;
536 exportPhotoPoint(point, inWriter, inExportImages, i, photoNum, absoluteAltitudes);
539 // Make a blob with description for each audio clip
540 if (point.getAudio() != null && writeAudios && writeCurrentPoint)
542 if (!writtenAudioHeader)
544 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>");
545 writtenAudioHeader = true;
547 exportAudioPoint(point, inWriter, absoluteAltitudes);
551 // Make a line for the track, if there is one
552 if (hasTrackpoints && writeTrack)
554 boolean useGxExtensions = _gxExtensionsRadio.isSelected();
557 // Write track using the Google Extensions to KML including gx:Track
558 numSaved += writeGxTrack(inWriter, absoluteAltitudes, selStart, selEnd);
561 // Write track using standard KML
562 numSaved += writeStandardTrack(inWriter, absoluteAltitudes, selStart, selEnd);
565 inWriter.write("</Folder>\n</kml>\n");
571 * Write out the track using standard KML LineString tag
572 * @param inWriter writer object to write to
573 * @param inAbsoluteAltitudes true to use absolute altitudes, false to clamp to ground
574 * @param inSelStart start index of selection, or -1 if whole track
575 * @param inSelEnd end index of selection, or -1 if whole track
576 * @return number of track points written
578 private int writeStandardTrack(OutputStreamWriter inWriter, boolean inAbsoluteAltitudes, int inSelStart,
583 // Set up strings for start and end of track segment
584 String trackStart = "\t<Placemark>\n\t\t<name>track</name>\n\t\t<Style>\n\t\t\t<LineStyle>\n"
585 + "\t\t\t\t<color>cc" + reverse(ColourUtils.makeHexCode(_colourPatch.getBackground())) + "</color>\n"
586 + "\t\t\t\t<width>4</width>\n\t\t\t</LineStyle>\n"
587 + "\t\t</Style>\n\t\t<LineString>\n";
588 if (inAbsoluteAltitudes) {
589 trackStart += "\t\t\t<extrude>1</extrude>\n\t\t\t<altitudeMode>absolute</altitudeMode>\n";
592 trackStart += "\t\t\t<altitudeMode>clampToGround</altitudeMode>\n";
594 trackStart += "\t\t\t<coordinates>";
595 String trackEnd = "\t\t\t</coordinates>\n\t\t</LineString>\n\t</Placemark>";
597 boolean justSelection = _pointTypeSelector.getJustSelection();
600 inWriter.write(trackStart);
601 // Loop over track points
602 boolean firstTrackpoint = true;
603 final int numPoints = _track.getNumPoints();
604 for (int i=0; i<numPoints; i++)
606 DataPoint point = _track.getPoint(i);
607 boolean writeCurrentPoint = !justSelection || (i>=inSelStart && i<=inSelEnd);
608 if (!point.isWaypoint() && writeCurrentPoint)
610 // start new track segment if necessary
611 if (point.getSegmentStart() && !firstTrackpoint) {
612 inWriter.write(trackEnd);
613 inWriter.write(trackStart);
615 if (point.getPhoto() == null)
617 exportTrackpoint(point, inWriter);
619 firstTrackpoint = false;
624 inWriter.write(trackEnd);
630 * Write out the track using Google's KML Extensions such as gx:Track
631 * @param inWriter writer object to write to
632 * @param inAbsoluteAltitudes true to use absolute altitudes, false to clamp to ground
633 * @param inSelStart start index of selection, or -1 if whole track
634 * @param inSelEnd end index of selection, or -1 if whole track
635 * @return number of track points written
637 private int writeGxTrack(OutputStreamWriter inWriter, boolean inAbsoluteAltitudes, int inSelStart,
642 // Set up strings for start and end of track segment
643 String trackStart = "\t<Placemark>\n\t\t<name>track</name>\n\t\t<Style>\n\t\t\t<LineStyle>\n"
644 + "\t\t\t\t<color>cc" + reverse(ColourUtils.makeHexCode(_colourPatch.getBackground())) + "</color>\n"
645 + "\t\t\t\t<width>4</width>\n\t\t\t</LineStyle>\n"
646 + "\t\t</Style>\n\t\t<gx:Track>\n";
647 if (inAbsoluteAltitudes) {
648 trackStart += "\t\t\t<extrude>1</extrude>\n\t\t\t<altitudeMode>absolute</altitudeMode>\n";
651 trackStart += "\t\t\t<altitudeMode>clampToGround</altitudeMode>\n";
653 String trackEnd = "\n\t\t</gx:Track>\n\t</Placemark>\n";
655 boolean justSelection = _pointTypeSelector.getJustSelection();
658 inWriter.write(trackStart);
659 StringBuilder whenList = new StringBuilder();
660 StringBuilder coordList = new StringBuilder();
662 // Loop over track points
663 boolean firstTrackpoint = true;
664 final int numPoints = _track.getNumPoints();
665 for (int i=0; i<numPoints; i++)
667 DataPoint point = _track.getPoint(i);
668 boolean writeCurrentPoint = !justSelection || (i>=inSelStart && i<=inSelEnd);
669 if (!point.isWaypoint() && writeCurrentPoint)
671 // start new track segment if necessary
672 if (point.getSegmentStart() && !firstTrackpoint)
674 inWriter.write(whenList.toString());
675 inWriter.write('\n');
676 inWriter.write(coordList.toString());
677 inWriter.write('\n');
678 inWriter.write(trackEnd);
679 whenList.setLength(0); coordList.setLength(0);
680 inWriter.write(trackStart);
682 if (point.getPhoto() == null)
684 // Add timestamp (if any) to the list
685 whenList.append("<when>");
686 if (point.hasTimestamp()) {
687 whenList.append(point.getTimestamp().getText(Timestamp.Format.ISO8601));
689 whenList.append("</when>\n");
690 // Add coordinates to the list
691 coordList.append("<gx:coord>");
692 coordList.append(point.getLongitude().output(Coordinate.FORMAT_DECIMAL_FORCE_POINT)).append(' ');
693 coordList.append(point.getLatitude().output(Coordinate.FORMAT_DECIMAL_FORCE_POINT)).append(' ');
694 if (point.hasAltitude()) {
695 coordList.append("" + point.getAltitude().getStringValue(UnitSetLibrary.UNITS_METRES));
698 coordList.append('0');
700 coordList.append("</gx:coord>\n");
702 firstTrackpoint = false;
707 inWriter.write(whenList.toString());
708 inWriter.write('\n');
709 inWriter.write(coordList.toString());
710 inWriter.write('\n');
711 inWriter.write(trackEnd);
717 * Reverse the hex code for the colours for KML's stupid backwards format
718 * @param inCode colour code rrggbb
719 * @return kml code bbggrr
721 private static String reverse(String inCode)
723 return inCode.substring(4, 6) + inCode.substring(2, 4) + inCode.substring(0, 2);
727 * Export the specified waypoint into the file
728 * @param inPoint waypoint to export
729 * @param inWriter writer object
730 * @param inAbsoluteAltitude true for absolute altitude
731 * @throws IOException on write failure
733 private void exportWaypoint(DataPoint inPoint, Writer inWriter, boolean inAbsoluteAltitude) throws IOException
735 String name = inPoint.getWaypointName().trim();
736 exportNamedPoint(inPoint, inWriter, name, inPoint.getFieldValue(Field.DESCRIPTION), null, inAbsoluteAltitude);
741 * Export the specified audio point into the file
742 * @param inPoint audio point to export
743 * @param inWriter writer object
744 * @param inAbsoluteAltitude true for absolute altitude
745 * @throws IOException on write failure
747 private void exportAudioPoint(DataPoint inPoint, Writer inWriter, boolean inAbsoluteAltitude) throws IOException
749 String name = inPoint.getAudio().getName();
751 if (inPoint.getAudio().getFile() != null) {
752 desc = inPoint.getAudio().getFile().getAbsolutePath();
754 exportNamedPoint(inPoint, inWriter, name, desc, "audio_icon", inAbsoluteAltitude);
759 * Export the specified photo into the file
760 * @param inPoint data point including photo
761 * @param inWriter writer object
762 * @param inImageLink flag to set whether to export image links or not
763 * @param inPointNumber number of point for accessing dimensions
764 * @param inImageNumber number of image for filename
765 * @param inAbsoluteAltitude true for absolute altitudes
766 * @throws IOException on write failure
768 private void exportPhotoPoint(DataPoint inPoint, Writer inWriter, boolean inImageLink,
769 int inPointNumber, int inImageNumber, boolean inAbsoluteAltitude)
772 String name = inPoint.getPhoto().getName();
776 Dimension imageSize = _imageDimensions[inPointNumber];
777 // Create html for the thumbnail images
778 desc = "<![CDATA[<br/><table border='0'><tr><td><center><img src='images/image"
779 + inImageNumber + ".jpg' width='" + imageSize.width + "' height='" + imageSize.height + "'></center></td></tr>"
780 + "<tr><td><center>" + name + "</center></td></tr></table>]]>";
783 exportNamedPoint(inPoint, inWriter, name, desc, "camera_icon", inAbsoluteAltitude);
788 * Export the specified named point into the file, like waypoint or photo point
789 * @param inPoint data point
790 * @param inWriter writer object
791 * @param inName name of point
792 * @param inDesc description of point, or null
793 * @param inStyle style of point, or null
794 * @param inAbsoluteAltitude true for absolute altitudes
795 * @throws IOException on write failure
797 private void exportNamedPoint(DataPoint inPoint, Writer inWriter, String inName,
798 String inDesc, String inStyle, boolean inAbsoluteAltitude)
801 inWriter.write("\t<Placemark>\n\t\t<name>");
802 inWriter.write(XmlUtils.fixCdata(inName));
803 inWriter.write("</name>\n");
806 // Write out description
807 inWriter.write("\t\t<description>");
808 inWriter.write(XmlUtils.fixCdata(inDesc));
809 inWriter.write("</description>\n");
813 inWriter.write("<styleUrl>#");
814 inWriter.write(inStyle);
815 inWriter.write("</styleUrl>\n");
817 inWriter.write("\t\t<Point>\n");
818 if (inAbsoluteAltitude && inPoint.hasAltitude()) {
819 inWriter.write("\t\t\t<altitudeMode>absolute</altitudeMode>\n");
822 inWriter.write("\t\t\t<altitudeMode>clampToGround</altitudeMode>\n");
824 inWriter.write("\t\t\t<coordinates>");
825 inWriter.write(inPoint.getLongitude().output(Coordinate.FORMAT_DECIMAL_FORCE_POINT));
827 inWriter.write(inPoint.getLatitude().output(Coordinate.FORMAT_DECIMAL_FORCE_POINT));
829 // Altitude if point has one
830 if (inPoint.hasAltitude()) {
831 inWriter.write("" + inPoint.getAltitude().getStringValue(UnitSetLibrary.UNITS_METRES));
836 inWriter.write("</coordinates>\n\t\t</Point>\n\t</Placemark>\n");
841 * Export the specified trackpoint into the file
842 * @param inPoint trackpoint to export
843 * @param inWriter writer object
845 private void exportTrackpoint(DataPoint inPoint, Writer inWriter) throws IOException
847 inWriter.write(inPoint.getLongitude().output(Coordinate.FORMAT_DECIMAL_FORCE_POINT));
849 inWriter.write(inPoint.getLatitude().output(Coordinate.FORMAT_DECIMAL_FORCE_POINT));
850 // Altitude if point has one
852 if (inPoint.hasAltitude()) {
853 inWriter.write("" + inPoint.getAltitude().getStringValue(UnitSetLibrary.UNITS_METRES));
858 inWriter.write('\n');
863 * Loop through the photos and create thumbnails
864 * @param inZipStream zip stream to save image files to
865 * @param inThumbSize thumbnail size
867 private void exportThumbnails(ZipOutputStream inZipStream, int inThumbSize)
870 // set up image writer
871 Iterator<ImageWriter> writers = ImageIO.getImageWritersByFormatName("jpg");
872 if (writers == null || !writers.hasNext())
874 throw new IOException("no JPEG writer found");
876 ImageWriter imageWriter = writers.next();
878 // Check selection checkbox
879 final boolean justSelection = _pointTypeSelector.getJustSelection();
880 int selStart = -1, selEnd = -1;
882 selStart = _trackInfo.getSelection().getStart();
883 selEnd = _trackInfo.getSelection().getEnd();
886 final int numPoints = _track.getNumPoints();
887 DataPoint point = null;
889 // Loop over all points in track
890 for (int i=0; i<numPoints && !_cancelPressed; i++)
892 point = _track.getPoint(i);
893 if (point.getPhoto() != null && point.getPhoto().isValid() && (!justSelection || (i>=selStart && i<=selEnd)))
896 // Make a new entry in zip file
897 ZipEntry entry = new ZipEntry("images/image" + photoNum + ".jpg");
898 inZipStream.putNextEntry(entry);
899 // Load image and write to outstream
900 ImageIcon icon = point.getPhoto().createImageIcon();
902 // Scale image to required size (not smoothed)
903 BufferedImage bufferedImage = ImageUtils.rotateImage(icon.getImage(),
904 inThumbSize, inThumbSize, point.getPhoto().getRotationDegrees());
905 // Store image dimensions so that it doesn't have to be calculated again for the points
906 _imageDimensions[i] = new Dimension(bufferedImage.getWidth(), bufferedImage.getHeight());
908 imageWriter.setOutput(ImageIO.createImageOutputStream(inZipStream));
909 imageWriter.write(bufferedImage);
910 // Close zip file entry
911 inZipStream.closeEntry();
912 // Update progress bar
913 _progressBar.setValue(photoNum+1);
920 * @return number of correlated photos in the track
922 private int getNumPhotosToExport()
924 int numPoints = _track.getNumPoints();
926 DataPoint point = null;
927 // Loop over all points in track
928 for (int i=0; i<numPoints; i++)
930 point = _track.getPoint(i);
931 if (point.getPhoto() != null) {