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.ImageIcon;
27 import javax.swing.JButton;
28 import javax.swing.JCheckBox;
29 import javax.swing.JDialog;
30 import javax.swing.JFileChooser;
31 import javax.swing.JLabel;
32 import javax.swing.JOptionPane;
33 import javax.swing.JPanel;
34 import javax.swing.JProgressBar;
35 import javax.swing.JTextField;
36 import javax.swing.SwingConstants;
39 import tim.prune.GenericFunction;
40 import tim.prune.I18nManager;
41 import tim.prune.UpdateMessageBroker;
42 import tim.prune.config.ColourUtils;
43 import tim.prune.config.Config;
44 import tim.prune.data.Altitude;
45 import tim.prune.data.Coordinate;
46 import tim.prune.data.DataPoint;
47 import tim.prune.data.Field;
48 import tim.prune.data.RecentFile;
49 import tim.prune.data.Track;
50 import tim.prune.data.TrackInfo;
51 import tim.prune.gui.ColourChooser;
52 import tim.prune.gui.ColourPatch;
53 import tim.prune.gui.DialogCloser;
54 import tim.prune.gui.ImageUtils;
55 import tim.prune.load.GenericFileFilter;
56 import tim.prune.save.xml.XmlUtils;
59 * Class to export track information
60 * into a specified Kml or Kmz file
62 public class KmlExporter extends GenericFunction implements Runnable
64 private TrackInfo _trackInfo = null;
65 private Track _track = null;
66 private JDialog _dialog = null;
67 private JTextField _descriptionField = null;
68 private PointTypeSelector _pointTypeSelector = null;
69 private JCheckBox _altitudesCheckbox = null;
70 private JCheckBox _kmzCheckbox = null;
71 private JCheckBox _exportImagesCheckbox = null;
72 private ColourPatch _colourPatch = null;
73 private JLabel _progressLabel = null;
74 private JProgressBar _progressBar = null;
75 private Dimension[] _imageDimensions = null;
76 private JFileChooser _fileChooser = null;
77 private File _exportFile = null;
78 private JButton _okButton = null;
79 private boolean _cancelPressed = false;
80 private ColourChooser _colourChooser = null;
82 // Filename of Kml file within zip archive
83 private static final String KML_FILENAME_IN_KMZ = "doc.kml";
84 // Default width and height of thumbnail images in Kmz
85 private static final int DEFAULT_THUMBNAIL_WIDTH = 240;
86 private static final int DEFAULT_THUMBNAIL_HEIGHT = 240;
87 // Default track colour
88 private static final Color DEFAULT_TRACK_COLOUR = new Color(204, 0, 0); // red
93 * @param inApp app object
95 public KmlExporter(App inApp)
98 _trackInfo = inApp.getTrackInfo();
99 _track = _trackInfo.getTrack();
103 public String getNameKey() {
104 return "function.exportkml";
108 * Show the dialog to select options and export file
112 // Make dialog window including whether to compress to kmz (and include pictures) or not
115 _dialog = new JDialog(_parentFrame, I18nManager.getText(getNameKey()), true);
116 _dialog.setLocationRelativeTo(_parentFrame);
117 _dialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE);
118 _dialog.getContentPane().add(makeDialogComponents());
120 _colourChooser = new ColourChooser(_dialog);
123 _descriptionField.setEnabled(true);
124 _okButton.setEnabled(true);
125 _progressLabel.setText("");
126 _progressBar.setVisible(false);
127 _dialog.setVisible(true);
132 * Create dialog components
133 * @return Panel containing all gui elements in dialog
135 private Component makeDialogComponents()
137 JPanel dialogPanel = new JPanel();
138 dialogPanel.setLayout(new BorderLayout(0, 5));
139 JPanel mainPanel = new JPanel();
140 mainPanel.setLayout(new BoxLayout(mainPanel, BoxLayout.Y_AXIS));
141 // Make a central panel with the text box and checkboxes
142 JPanel descPanel = new JPanel();
143 descPanel.setLayout(new FlowLayout());
144 descPanel.add(new JLabel(I18nManager.getText("dialog.exportkml.text")));
145 _descriptionField = new JTextField(20);
146 _descriptionField.addKeyListener(new DialogCloser(_dialog));
147 descPanel.add(_descriptionField);
148 descPanel.setAlignmentX(Component.CENTER_ALIGNMENT);
149 mainPanel.add(descPanel);
150 dialogPanel.add(mainPanel, BorderLayout.CENTER);
151 // point type selection
152 _pointTypeSelector = new PointTypeSelector();
153 _pointTypeSelector.setAlignmentX(Component.CENTER_ALIGNMENT);
154 mainPanel.add(_pointTypeSelector);
156 Color trackColour = ColourUtils.colourFromHex(Config.getConfigString(Config.KEY_KML_TRACK_COLOUR));
157 if (trackColour == null) {
158 trackColour = DEFAULT_TRACK_COLOUR;
160 _colourPatch = new ColourPatch(trackColour);
161 _colourPatch.addMouseListener(new MouseAdapter() {
162 public void mouseClicked(MouseEvent e) {
163 _colourChooser.showDialog(_colourPatch.getBackground());
164 Color colour = _colourChooser.getChosenColour();
165 if (colour != null) _colourPatch.setColour(colour);
168 JPanel colourPanel = new JPanel();
169 colourPanel.add(new JLabel(I18nManager.getText("dialog.exportkml.trackcolour")));
170 colourPanel.add(_colourPatch);
171 mainPanel.add(colourPanel);
172 // Checkbox for altitude export
173 _altitudesCheckbox = new JCheckBox(I18nManager.getText("dialog.exportkml.altitude"));
174 _altitudesCheckbox.setHorizontalTextPosition(SwingConstants.LEFT);
175 _altitudesCheckbox.setAlignmentX(Component.CENTER_ALIGNMENT);
176 mainPanel.add(_altitudesCheckbox);
177 // Checkboxes for kmz export and image export
178 _kmzCheckbox = new JCheckBox(I18nManager.getText("dialog.exportkml.kmz"));
179 _kmzCheckbox.setHorizontalTextPosition(SwingConstants.LEFT);
180 _kmzCheckbox.setAlignmentX(Component.CENTER_ALIGNMENT);
181 _kmzCheckbox.addActionListener(new ActionListener() {
182 public void actionPerformed(ActionEvent e)
184 // enable image checkbox if kmz activated
188 mainPanel.add(_kmzCheckbox);
189 _exportImagesCheckbox = new JCheckBox(I18nManager.getText("dialog.exportkml.exportimages"));
190 _exportImagesCheckbox.setHorizontalTextPosition(SwingConstants.LEFT);
191 _exportImagesCheckbox.setAlignmentX(Component.CENTER_ALIGNMENT);
192 mainPanel.add(_exportImagesCheckbox);
193 mainPanel.add(Box.createVerticalStrut(10));
194 _progressLabel = new JLabel("...");
195 _progressLabel.setAlignmentX(Component.CENTER_ALIGNMENT);
196 mainPanel.add(_progressLabel);
197 _progressBar = new JProgressBar(0, 100);
198 _progressBar.setVisible(false);
199 _progressBar.setAlignmentX(Component.CENTER_ALIGNMENT);
200 mainPanel.add(_progressBar);
201 mainPanel.add(Box.createVerticalStrut(10));
202 // button panel at bottom
203 JPanel buttonPanel = new JPanel();
204 buttonPanel.setLayout(new FlowLayout(FlowLayout.RIGHT));
205 _okButton = new JButton(I18nManager.getText("button.ok"));
206 ActionListener okListener = new ActionListener() {
207 public void actionPerformed(ActionEvent e)
212 _okButton.addActionListener(okListener);
213 _descriptionField.addActionListener(okListener);
214 buttonPanel.add(_okButton);
215 JButton cancelButton = new JButton(I18nManager.getText("button.cancel"));
216 cancelButton.addActionListener(new ActionListener() {
217 public void actionPerformed(ActionEvent e)
219 _cancelPressed = true;
223 buttonPanel.add(cancelButton);
224 dialogPanel.add(buttonPanel, BorderLayout.SOUTH);
230 * Enable the checkboxes according to data
232 private void enableCheckboxes()
234 _pointTypeSelector.init(_trackInfo);
235 boolean hasAltitudes = _track.hasData(Field.ALTITUDE);
236 if (!hasAltitudes) {_altitudesCheckbox.setSelected(false);}
237 boolean hasPhotos = _trackInfo.getPhotoList() != null && _trackInfo.getPhotoList().getNumPhotos() > 0;
238 _exportImagesCheckbox.setSelected(hasPhotos && _kmzCheckbox.isSelected());
239 _exportImagesCheckbox.setEnabled(hasPhotos && _kmzCheckbox.isSelected());
244 * Start the export process based on the input parameters
246 private void startExport()
248 // OK pressed, now validate selection checkboxes
249 if (!_pointTypeSelector.getAnythingSelected()) {
250 JOptionPane.showMessageDialog(_parentFrame, I18nManager.getText("dialog.save.notypesselected"),
251 I18nManager.getText("dialog.saveoptions.title"), JOptionPane.WARNING_MESSAGE);
254 // Choose output file
255 if (_fileChooser == null)
257 _fileChooser = new JFileChooser();
258 _fileChooser.setDialogType(JFileChooser.SAVE_DIALOG);
259 _fileChooser.setFileFilter(new GenericFileFilter("filetype.kmlkmz", new String[] {"kml", "kmz"}));
260 // start from directory in config which should be set
261 String configDir = Config.getConfigString(Config.KEY_TRACK_DIR);
262 if (configDir != null) {_fileChooser.setCurrentDirectory(new File(configDir));}
264 String requiredExtension = null, otherExtension = null;
265 if (_kmzCheckbox.isSelected()) {
266 requiredExtension = ".kmz"; otherExtension = ".kml";
269 requiredExtension = ".kml"; otherExtension = ".kmz";
271 _fileChooser.setAcceptAllFileFilterUsed(false);
272 // Allow choose again if an existing file is selected
273 boolean chooseAgain = false;
277 if (_fileChooser.showSaveDialog(_parentFrame) == JFileChooser.APPROVE_OPTION)
279 // OK pressed and file chosen
280 File file = _fileChooser.getSelectedFile();
281 if (file.getName().toLowerCase().endsWith(otherExtension))
283 String path = file.getAbsolutePath();
284 file = new File(path.substring(0, path.length()-otherExtension.length()) + requiredExtension);
286 else if (!file.getName().toLowerCase().endsWith(requiredExtension))
288 file = new File(file.getAbsolutePath() + requiredExtension);
290 // Check if file exists and if necessary prompt for overwrite
291 Object[] buttonTexts = {I18nManager.getText("button.overwrite"), I18nManager.getText("button.cancel")};
292 if (!file.exists() || JOptionPane.showOptionDialog(_parentFrame,
293 I18nManager.getText("dialog.save.overwrite.text"),
294 I18nManager.getText("dialog.save.overwrite.title"), JOptionPane.YES_NO_OPTION,
295 JOptionPane.WARNING_MESSAGE, null, buttonTexts, buttonTexts[1])
296 == JOptionPane.YES_OPTION)
298 // New file or overwrite confirmed, so initiate export in separate thread
300 _cancelPressed = false;
301 new Thread(this).start();
308 } while (chooseAgain);
313 * Run method for controlling separate thread for exporting
317 // Disable ok button to stop second go
318 _okButton.setEnabled(false);
319 _descriptionField.setEnabled(false);
320 // Initialise progress indicators
321 _progressLabel.setText(I18nManager.getText("confirm.running"));
322 _progressBar.setVisible(true);
323 _progressBar.setValue(0);
324 boolean exportToKmz = _kmzCheckbox.isSelected();
325 boolean exportImages = exportToKmz && _exportImagesCheckbox.isSelected();
326 _progressBar.setMaximum(exportImages?getNumPhotosToExport():1);
328 // Determine photo thumbnail size from config
329 int thumbWidth = Config.getConfigInt(Config.KEY_KMZ_IMAGE_WIDTH);
330 if (thumbWidth < DEFAULT_THUMBNAIL_WIDTH) {thumbWidth = DEFAULT_THUMBNAIL_WIDTH;}
331 int thumbHeight = Config.getConfigInt(Config.KEY_KMZ_IMAGE_HEIGHT);
332 if (thumbHeight < DEFAULT_THUMBNAIL_HEIGHT) {thumbHeight = DEFAULT_THUMBNAIL_HEIGHT;}
333 // Create array for image dimensions in case it's required
334 _imageDimensions = new Dimension[_track.getNumPoints()];
336 OutputStreamWriter writer = null;
337 ZipOutputStream zipOutputStream = null;
340 // Select writer according to whether kmz requested or not
341 if (!_kmzCheckbox.isSelected())
343 // normal writing to file
344 writer = new OutputStreamWriter(new FileOutputStream(_exportFile));
348 // kmz requested - need zip output stream
349 zipOutputStream = new ZipOutputStream(new FileOutputStream(_exportFile));
350 // Export images into zip file too if requested
353 // Create thumbnails of each photo in turn and add to zip as images/image<n>.jpg
354 // This is done first so that photo sizes are known for later
355 exportThumbnails(zipOutputStream, thumbWidth, thumbHeight);
357 writer = new OutputStreamWriter(zipOutputStream);
358 // Make an entry in the zip file for the kml file
359 ZipEntry kmlEntry = new ZipEntry(KML_FILENAME_IN_KMZ);
360 zipOutputStream.putNextEntry(kmlEntry);
363 final int numPoints = exportData(writer, exportImages);
364 // update config with selected track colour
365 Config.setConfigString(Config.KEY_KML_TRACK_COLOUR, ColourUtils.makeHexCode(_colourPatch.getBackground()));
366 // update progress bar
367 _progressBar.setValue(1);
369 // close zip entry if necessary
370 if (zipOutputStream != null)
372 // Make sure all buffered data in writer is flushed
374 // Close off this entry in the zip file
375 zipOutputStream.closeEntry();
380 _imageDimensions = null;
381 // Store directory in config for later
382 Config.setConfigString(Config.KEY_TRACK_DIR, _exportFile.getParentFile().getAbsolutePath());
383 // Add to recent file list
384 Config.getRecentFileList().addFile(new RecentFile(_exportFile, true));
386 UpdateMessageBroker.informSubscribers();
387 UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.save.ok1")
388 + " " + numPoints + " " + I18nManager.getText("confirm.save.ok2")
389 + " " + _exportFile.getAbsolutePath());
390 // export successful so need to close dialog and return
394 catch (IOException ioe)
397 if (writer != null) writer.close();
399 catch (IOException ioe2) {}
400 JOptionPane.showMessageDialog(_parentFrame,
401 I18nManager.getText("error.save.failed") + " : " + ioe.getMessage(),
402 I18nManager.getText("error.save.dialogtitle"), JOptionPane.ERROR_MESSAGE);
404 // if not returned already, export failed so need to recall the file selection
410 * Export the information to the given writer
411 * @param inWriter writer object
412 * @param inExportImages true if image thumbnails are to be referenced
413 * @return number of points written
415 private int exportData(OutputStreamWriter inWriter, boolean inExportImages)
418 boolean writeTrack = _pointTypeSelector.getTrackpointsSelected();
419 boolean writeWaypoints = _pointTypeSelector.getWaypointsSelected();
420 boolean writePhotos = _pointTypeSelector.getPhotopointsSelected();
421 boolean writeAudios = _pointTypeSelector.getAudiopointsSelected();
422 boolean justSelection = _pointTypeSelector.getJustSelection();
423 inWriter.write("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<kml xmlns=\"http://earth.google.com/kml/2.1\">\n<Folder>\n");
424 inWriter.write("\t<name>");
425 if (_descriptionField != null && _descriptionField.getText() != null && !_descriptionField.getText().equals(""))
427 inWriter.write(_descriptionField.getText());
430 inWriter.write("Export from GpsPrune");
432 inWriter.write("</name>\n");
434 // Examine selection if required
435 int selStart = -1, selEnd = -1;
437 selStart = _trackInfo.getSelection().getStart();
438 selEnd = _trackInfo.getSelection().getEnd();
441 boolean absoluteAltitudes = _altitudesCheckbox.isSelected();
443 DataPoint point = null;
444 boolean hasTrackpoints = false;
445 boolean writtenPhotoHeader = false, writtenAudioHeader = false;
446 final int numPoints = _track.getNumPoints();
449 // Loop over waypoints
450 for (i=0; i<numPoints; i++)
452 point = _track.getPoint(i);
453 boolean writeCurrentPoint = !justSelection || (i>=selStart && i<=selEnd);
454 // Make a blob for each waypoint
455 if (point.isWaypoint())
457 if (writeWaypoints && writeCurrentPoint)
459 exportWaypoint(point, inWriter, absoluteAltitudes);
463 else if (!point.hasMedia())
465 hasTrackpoints = true;
467 // Make a blob with description for each photo
468 // Photos have already been written so picture sizes already known
469 if (point.getPhoto() != null && writePhotos && writeCurrentPoint)
471 if (!writtenPhotoHeader)
473 inWriter.write("<Style id=\"camera_icon\"><IconStyle><Icon><href>http://maps.google.com/mapfiles/kml/pal4/icon46.png</href></Icon></IconStyle></Style>");
474 writtenPhotoHeader = true;
477 exportPhotoPoint(point, inWriter, inExportImages, i, photoNum, absoluteAltitudes);
480 // Make a blob with description for each audio clip
481 if (point.getAudio() != null && writeAudios && writeCurrentPoint)
483 if (!writtenAudioHeader)
485 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>");
486 writtenAudioHeader = true;
488 exportAudioPoint(point, inWriter, absoluteAltitudes);
492 // Make a line for the track, if there is one
493 if (hasTrackpoints && writeTrack)
495 // Set up strings for start and end of track segment
496 String trackStart = "\t<Placemark>\n\t\t<name>track</name>\n\t\t<Style>\n\t\t\t<LineStyle>\n"
497 + "\t\t\t\t<color>cc" + reverse(ColourUtils.makeHexCode(_colourPatch.getBackground())) + "</color>\n"
498 + "\t\t\t\t<width>4</width>\n\t\t\t</LineStyle>\n"
499 + "\t\t\t<PolyStyle><color>33cc0000</color></PolyStyle>\n"
500 + "\t\t</Style>\n\t\t<LineString>\n";
501 if (absoluteAltitudes) {
502 trackStart += "\t\t\t<extrude>1</extrude>\n\t\t\t<altitudeMode>absolute</altitudeMode>\n";
505 trackStart += "\t\t\t<altitudeMode>clampToGround</altitudeMode>\n";
507 trackStart += "\t\t\t<coordinates>";
508 String trackEnd = "\t\t\t</coordinates>\n\t\t</LineString>\n\t</Placemark>";
511 inWriter.write(trackStart);
512 // Loop over track points
513 boolean firstTrackpoint = true;
514 for (i=0; i<numPoints; i++)
516 point = _track.getPoint(i);
517 boolean writeCurrentPoint = !justSelection || (i>=selStart && i<=selEnd);
518 if (!point.isWaypoint() && writeCurrentPoint)
520 // start new track segment if necessary
521 if (point.getSegmentStart() && !firstTrackpoint) {
522 inWriter.write(trackEnd);
523 inWriter.write(trackStart);
525 if (point.getPhoto() == null)
527 exportTrackpoint(point, inWriter);
529 firstTrackpoint = false;
534 inWriter.write(trackEnd);
536 inWriter.write("</Folder>\n</kml>");
541 * Reverse the hex code for the colours for KML's stupid backwards format
542 * @param inCode colour code rrggbb
543 * @return kml code bbggrr
545 private static String reverse(String inCode)
547 return inCode.substring(4, 6) + inCode.substring(2, 4) + inCode.substring(0, 2);
551 * Export the specified waypoint into the file
552 * @param inPoint waypoint to export
553 * @param inWriter writer object
554 * @param inAbsoluteAltitude true for absolute altitude
555 * @throws IOException on write failure
557 private void exportWaypoint(DataPoint inPoint, Writer inWriter, boolean inAbsoluteAltitude) throws IOException
559 String name = inPoint.getWaypointName().trim();
560 exportNamedPoint(inPoint, inWriter, name, inPoint.getFieldValue(Field.DESCRIPTION), null, inAbsoluteAltitude);
565 * Export the specified audio point into the file
566 * @param inPoint audio point to export
567 * @param inWriter writer object
568 * @param inAbsoluteAltitude true for absolute altitude
569 * @throws IOException on write failure
571 private void exportAudioPoint(DataPoint inPoint, Writer inWriter, boolean inAbsoluteAltitude) throws IOException
573 String name = inPoint.getAudio().getName();
575 if (inPoint.getAudio().getFile() != null) {
576 desc = inPoint.getAudio().getFile().getAbsolutePath();
578 exportNamedPoint(inPoint, inWriter, name, desc, "audio_icon", inAbsoluteAltitude);
583 * Export the specified photo into the file
584 * @param inPoint data point including photo
585 * @param inWriter writer object
586 * @param inImageLink flag to set whether to export image links or not
587 * @param inPointNumber number of point for accessing dimensions
588 * @param inImageNumber number of image for filename
589 * @param inAbsoluteAltitude true for absolute altitudes
590 * @throws IOException on write failure
592 private void exportPhotoPoint(DataPoint inPoint, Writer inWriter, boolean inImageLink,
593 int inPointNumber, int inImageNumber, boolean inAbsoluteAltitude)
596 String name = inPoint.getPhoto().getName();
600 Dimension imageSize = _imageDimensions[inPointNumber];
601 // Create html for the thumbnail images
602 desc = "<![CDATA[<br/><table border='0'><tr><td><center><img src='images/image"
603 + inImageNumber + ".jpg' width='" + imageSize.width + "' height='" + imageSize.height + "'></center></td></tr>"
604 + "<tr><td><center>" + name + "</center></td></tr></table>]]>";
607 exportNamedPoint(inPoint, inWriter, name, desc, "camera_icon", inAbsoluteAltitude);
612 * Export the specified named point into the file, like waypoint or photo point
613 * @param inPoint data point
614 * @param inWriter writer object
615 * @param inName name of point
616 * @param inDesc description of point, or null
617 * @param inStyle style of point, or null
618 * @param inAbsoluteAltitude true for absolute altitudes
619 * @throws IOException on write failure
621 private void exportNamedPoint(DataPoint inPoint, Writer inWriter, String inName,
622 String inDesc, String inStyle, boolean inAbsoluteAltitude)
625 inWriter.write("\t<Placemark>\n\t\t<name>");
626 inWriter.write(inName);
627 inWriter.write("</name>\n");
630 // Write out description
631 inWriter.write("\t\t<description>");
632 inWriter.write(XmlUtils.fixCdata(inDesc));
633 inWriter.write("</description>\n");
637 inWriter.write("<styleUrl>#");
638 inWriter.write(inStyle);
639 inWriter.write("</styleUrl>\n");
641 inWriter.write("\t\t<Point>\n");
642 if (inAbsoluteAltitude && inPoint.hasAltitude()) {
643 inWriter.write("\t\t\t<altitudeMode>absolute</altitudeMode>\n");
646 inWriter.write("\t\t\t<altitudeMode>clampToGround</altitudeMode>\n");
648 inWriter.write("\t\t\t<coordinates>");
649 inWriter.write(inPoint.getLongitude().output(Coordinate.FORMAT_DECIMAL_FORCE_POINT));
651 inWriter.write(inPoint.getLatitude().output(Coordinate.FORMAT_DECIMAL_FORCE_POINT));
653 // Altitude if point has one
654 if (inPoint.hasAltitude()) {
655 inWriter.write("" + inPoint.getAltitude().getStringValue(Altitude.Format.METRES));
660 inWriter.write("</coordinates>\n\t\t</Point>\n\t</Placemark>\n");
665 * Export the specified trackpoint into the file
666 * @param inPoint trackpoint to export
667 * @param inWriter writer object
669 private void exportTrackpoint(DataPoint inPoint, Writer inWriter) throws IOException
671 inWriter.write(inPoint.getLongitude().output(Coordinate.FORMAT_DECIMAL_FORCE_POINT));
673 inWriter.write(inPoint.getLatitude().output(Coordinate.FORMAT_DECIMAL_FORCE_POINT));
674 // Altitude if point has one
676 if (inPoint.hasAltitude()) {
677 inWriter.write("" + inPoint.getAltitude().getStringValue(Altitude.Format.METRES));
682 inWriter.write('\n');
687 * Loop through the photos and create thumbnails
688 * @param inZipStream zip stream to save image files to
689 * @param inThumbWidth thumbnail width
690 * @param inThumbHeight thumbnail height
692 private void exportThumbnails(ZipOutputStream inZipStream, int inThumbWidth, int inThumbHeight)
695 // set up image writer
696 Iterator<ImageWriter> writers = ImageIO.getImageWritersByFormatName("jpg");
697 if (writers == null || !writers.hasNext())
699 throw new IOException("no JPEG writer found");
701 ImageWriter imageWriter = writers.next();
703 // Check selection checkbox
704 boolean justSelection = _pointTypeSelector.getJustSelection();
705 int selStart = -1, selEnd = -1;
707 selStart = _trackInfo.getSelection().getStart();
708 selEnd = _trackInfo.getSelection().getEnd();
711 int numPoints = _track.getNumPoints();
712 DataPoint point = null;
714 // Loop over all points in track
715 for (int i=0; i<numPoints && !_cancelPressed; i++)
717 point = _track.getPoint(i);
718 if (point.getPhoto() != null && (!justSelection || (i>=selStart && i<=selEnd)))
721 // Make a new entry in zip file
722 ZipEntry entry = new ZipEntry("images/image" + photoNum + ".jpg");
723 inZipStream.putNextEntry(entry);
724 // Load image and write to outstream
725 ImageIcon icon = point.getPhoto().createImageIcon();
727 // Scale image to required size TODO: should it also be smoothed, or only if it's smaller than a certain size?
728 BufferedImage bufferedImage = ImageUtils.rotateImage(icon.getImage(),
729 inThumbWidth, inThumbHeight, point.getPhoto().getRotationDegrees());
730 // Store image dimensions so that it doesn't have to be calculated again for the points
731 _imageDimensions[i] = new Dimension(bufferedImage.getWidth(), bufferedImage.getHeight());
733 imageWriter.setOutput(ImageIO.createImageOutputStream(inZipStream));
734 imageWriter.write(bufferedImage);
735 // Close zip file entry
736 inZipStream.closeEntry();
737 // Update progress bar
738 _progressBar.setValue(photoNum+1);
745 * @return number of correlated photos in the track
747 private int getNumPhotosToExport()
749 int numPoints = _track.getNumPoints();
751 DataPoint point = null;
752 // Loop over all points in track
753 for (int i=0; i<numPoints; i++)
755 point = _track.getPoint(i);
756 if (point.getPhoto() != null) {