]> gitweb.fperrin.net Git - GpsPrune.git/blob - tim/prune/save/KmlExporter.java
Version 9, February 2010
[GpsPrune.git] / tim / prune / save / KmlExporter.java
1 package tim.prune.save;
2
3 import java.awt.BorderLayout;
4 import java.awt.Color;
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;
13 import java.io.File;
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;
21
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;
37
38 import tim.prune.App;
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.Track;
49 import tim.prune.data.TrackInfo;
50 import tim.prune.gui.ColourChooser;
51 import tim.prune.gui.ColourPatch;
52 import tim.prune.gui.ImageUtils;
53 import tim.prune.load.GenericFileFilter;
54
55 /**
56  * Class to export track information
57  * into a specified Kml or Kmz file
58  */
59 public class KmlExporter extends GenericFunction implements Runnable
60 {
61         private TrackInfo _trackInfo = null;
62         private Track _track = null;
63         private JDialog _dialog = null;
64         private JTextField _descriptionField = null;
65         private PointTypeSelector _pointTypeSelector = null;
66         private JCheckBox _altitudesCheckbox = null;
67         private JCheckBox _kmzCheckbox = null;
68         private JCheckBox _exportImagesCheckbox = null;
69         private ColourPatch _colourPatch = null;
70         private JLabel _progressLabel = null;
71         private JProgressBar _progressBar = null;
72         private Dimension[] _imageDimensions = null;
73         private JFileChooser _fileChooser = null;
74         private File _exportFile = null;
75         private JButton _okButton = null;
76         private boolean _cancelPressed = false;
77         private ColourChooser _colourChooser = null;
78
79         // Filename of Kml file within zip archive
80         private static final String KML_FILENAME_IN_KMZ = "doc.kml";
81         // Default width and height of thumbnail images in Kmz
82         private static final int DEFAULT_THUMBNAIL_WIDTH = 240;
83         private static final int DEFAULT_THUMBNAIL_HEIGHT = 240;
84         // Actual selected width and height of thumbnail images in Kmz
85         private static int THUMBNAIL_WIDTH = 0;
86         private static int THUMBNAIL_HEIGHT = 0;
87         // Default track colour
88         private static final Color DEFAULT_TRACK_COLOUR = new Color(204, 0, 0); // red
89
90
91         /**
92          * Constructor
93          * @param inApp app object
94          */
95         public KmlExporter(App inApp)
96         {
97                 super(inApp);
98                 _trackInfo = inApp.getTrackInfo();
99                 _track = _trackInfo.getTrack();
100         }
101
102         /** Get name key */
103         public String getNameKey() {
104                 return "function.exportkml";
105         }
106
107         /**
108          * Show the dialog to select options and export file
109          */
110         public void begin()
111         {
112                 // Make dialog window including whether to compress to kmz (and include pictures) or not
113                 if (_dialog == null)
114                 {
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());
119                         _dialog.pack();
120                         _colourChooser = new ColourChooser(_dialog);
121                 }
122                 enableCheckboxes();
123                 _descriptionField.setEnabled(true);
124                 _okButton.setEnabled(true);
125                 _progressLabel.setText("");
126                 _progressBar.setVisible(false);
127                 _dialog.setVisible(true);
128         }
129
130
131         /**
132          * Create dialog components
133          * @return Panel containing all gui elements in dialog
134          */
135         private Component makeDialogComponents()
136         {
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                 descPanel.add(_descriptionField);
147                 descPanel.setAlignmentX(Component.CENTER_ALIGNMENT);
148                 mainPanel.add(descPanel);
149                 dialogPanel.add(mainPanel, BorderLayout.CENTER);
150                 // point type selection
151                 _pointTypeSelector = new PointTypeSelector();
152                 _pointTypeSelector.setAlignmentX(Component.CENTER_ALIGNMENT);
153                 mainPanel.add(_pointTypeSelector);
154                 // Colour definition
155                 Color trackColour = ColourUtils.colourFromHex(Config.getConfigString(Config.KEY_KML_TRACK_COLOUR));
156                 if (trackColour == null) {
157                         trackColour = DEFAULT_TRACK_COLOUR;
158                 }
159                 _colourPatch = new ColourPatch(trackColour);
160                 _colourPatch.addMouseListener(new MouseAdapter() {
161                         public void mouseClicked(MouseEvent e) {
162                                 _colourChooser.showDialog(_colourPatch.getBackground());
163                                 Color colour = _colourChooser.getChosenColour();
164                                 if (colour != null) _colourPatch.setColour(colour);
165                         }
166                 });
167                 JPanel colourPanel = new JPanel();
168                 colourPanel.add(new JLabel(I18nManager.getText("dialog.exportkml.trackcolour")));
169                 colourPanel.add(_colourPatch);
170                 mainPanel.add(colourPanel);
171                 // Checkbox for altitude export
172                 _altitudesCheckbox = new JCheckBox(I18nManager.getText("dialog.exportkml.altitude"));
173                 _altitudesCheckbox.setHorizontalTextPosition(SwingConstants.LEFT);
174                 _altitudesCheckbox.setAlignmentX(Component.CENTER_ALIGNMENT);
175                 mainPanel.add(_altitudesCheckbox);
176                 // Checkboxes for kmz export and image export
177                 _kmzCheckbox = new JCheckBox(I18nManager.getText("dialog.exportkml.kmz"));
178                 _kmzCheckbox.setHorizontalTextPosition(SwingConstants.LEFT);
179                 _kmzCheckbox.setAlignmentX(Component.CENTER_ALIGNMENT);
180                 _kmzCheckbox.addActionListener(new ActionListener() {
181                         public void actionPerformed(ActionEvent e)
182                         {
183                                 // enable image checkbox if kmz activated
184                                 enableCheckboxes();
185                         }
186                 });
187                 mainPanel.add(_kmzCheckbox);
188                 _exportImagesCheckbox = new JCheckBox(I18nManager.getText("dialog.exportkml.exportimages"));
189                 _exportImagesCheckbox.setHorizontalTextPosition(SwingConstants.LEFT);
190                 _exportImagesCheckbox.setAlignmentX(Component.CENTER_ALIGNMENT);
191                 mainPanel.add(_exportImagesCheckbox);
192                 mainPanel.add(Box.createVerticalStrut(10));
193                 _progressLabel = new JLabel("...");
194                 _progressLabel.setAlignmentX(Component.CENTER_ALIGNMENT);
195                 mainPanel.add(_progressLabel);
196                 _progressBar = new JProgressBar(0, 100);
197                 _progressBar.setVisible(false);
198                 _progressBar.setAlignmentX(Component.CENTER_ALIGNMENT);
199                 mainPanel.add(_progressBar);
200                 mainPanel.add(Box.createVerticalStrut(10));
201                 // button panel at bottom
202                 JPanel buttonPanel = new JPanel();
203                 buttonPanel.setLayout(new FlowLayout(FlowLayout.RIGHT));
204                 _okButton = new JButton(I18nManager.getText("button.ok"));
205                 ActionListener okListener = new ActionListener() {
206                         public void actionPerformed(ActionEvent e)
207                         {
208                                 startExport();
209                         }
210                 };
211                 _okButton.addActionListener(okListener);
212                 _descriptionField.addActionListener(okListener);
213                 buttonPanel.add(_okButton);
214                 JButton cancelButton = new JButton(I18nManager.getText("button.cancel"));
215                 cancelButton.addActionListener(new ActionListener() {
216                         public void actionPerformed(ActionEvent e)
217                         {
218                                 _cancelPressed = true;
219                                 _dialog.dispose();
220                         }
221                 });
222                 buttonPanel.add(cancelButton);
223                 dialogPanel.add(buttonPanel, BorderLayout.SOUTH);
224                 return dialogPanel;
225         }
226
227
228         /**
229          * Enable the checkboxes according to data
230          */
231         private void enableCheckboxes()
232         {
233                 _pointTypeSelector.init(_trackInfo);
234                 boolean hasAltitudes = _track.hasData(Field.ALTITUDE);
235                 if (!hasAltitudes) {_altitudesCheckbox.setSelected(false);}
236                 boolean hasPhotos = _trackInfo.getPhotoList() != null && _trackInfo.getPhotoList().getNumPhotos() > 0;
237                 _exportImagesCheckbox.setSelected(hasPhotos && _kmzCheckbox.isSelected());
238                 _exportImagesCheckbox.setEnabled(hasPhotos && _kmzCheckbox.isSelected());
239         }
240
241
242         /**
243          * Start the export process based on the input parameters
244          */
245         private void startExport()
246         {
247                 // OK pressed, now validate selection checkboxes
248                 if (!_pointTypeSelector.getAnythingSelected()) {
249                         JOptionPane.showMessageDialog(_parentFrame, I18nManager.getText("dialog.save.notypesselected"),
250                                 I18nManager.getText("dialog.saveoptions.title"), JOptionPane.WARNING_MESSAGE);
251                         return;
252                 }
253                 // Choose output file
254                 if (_fileChooser == null)
255                 {
256                         _fileChooser = new JFileChooser();
257                         _fileChooser.setDialogType(JFileChooser.SAVE_DIALOG);
258                         _fileChooser.setFileFilter(new GenericFileFilter("filetype.kmlkmz", new String[] {"kml", "kmz"}));
259                         // start from directory in config which should be set
260                         String configDir = Config.getConfigString(Config.KEY_TRACK_DIR);
261                         if (configDir != null) {_fileChooser.setCurrentDirectory(new File(configDir));}
262                 }
263                 String requiredExtension = null, otherExtension = null;
264                 if (_kmzCheckbox.isSelected()) {
265                         requiredExtension = ".kmz"; otherExtension = ".kml";
266                 }
267                 else {
268                         requiredExtension = ".kml"; otherExtension = ".kmz";
269                 }
270                 _fileChooser.setAcceptAllFileFilterUsed(false);
271                 // Allow choose again if an existing file is selected
272                 boolean chooseAgain = false;
273                 do
274                 {
275                         chooseAgain = false;
276                         if (_fileChooser.showSaveDialog(_parentFrame) == JFileChooser.APPROVE_OPTION)
277                         {
278                                 // OK pressed and file chosen
279                                 File file = _fileChooser.getSelectedFile();
280                                 if (file.getName().toLowerCase().endsWith(otherExtension))
281                                 {
282                                         String path = file.getAbsolutePath();
283                                         file = new File(path.substring(0, path.length()-otherExtension.length()) + requiredExtension);
284                                 }
285                                 else if (!file.getName().toLowerCase().endsWith(requiredExtension))
286                                 {
287                                         file = new File(file.getAbsolutePath() + requiredExtension);
288                                 }
289                                 // Check if file exists and if necessary prompt for overwrite
290                                 Object[] buttonTexts = {I18nManager.getText("button.overwrite"), I18nManager.getText("button.cancel")};
291                                 if (!file.exists() || JOptionPane.showOptionDialog(_parentFrame,
292                                                 I18nManager.getText("dialog.save.overwrite.text"),
293                                                 I18nManager.getText("dialog.save.overwrite.title"), JOptionPane.YES_NO_OPTION,
294                                                 JOptionPane.WARNING_MESSAGE, null, buttonTexts, buttonTexts[1])
295                                         == JOptionPane.YES_OPTION)
296                                 {
297                                         // New file or overwrite confirmed, so initiate export in separate thread
298                                         _exportFile = file;
299                                         _cancelPressed = false;
300                                         new Thread(this).start();
301                                 }
302                                 else
303                                 {
304                                         chooseAgain = true;
305                                 }
306                         }
307                 } while (chooseAgain);
308         }
309
310
311         /**
312          * Run method for controlling separate thread for exporting
313          */
314         public void run()
315         {
316                 // Disable ok button to stop second go
317                 _okButton.setEnabled(false);
318                 _descriptionField.setEnabled(false);
319                 // Initialise progress indicators
320                 _progressLabel.setText(I18nManager.getText("confirm.running"));
321                 _progressBar.setVisible(true);
322                 _progressBar.setValue(0);
323                 boolean exportToKmz = _kmzCheckbox.isSelected();
324                 boolean exportImages = exportToKmz && _exportImagesCheckbox.isSelected();
325                 _progressBar.setMaximum(exportImages?getNumPhotosToExport():1);
326
327                 // Determine photo thumbnail size from config
328                 THUMBNAIL_WIDTH = Config.getConfigInt(Config.KEY_KMZ_IMAGE_WIDTH);
329                 if (THUMBNAIL_WIDTH < DEFAULT_THUMBNAIL_WIDTH) {THUMBNAIL_WIDTH = DEFAULT_THUMBNAIL_WIDTH;}
330                 THUMBNAIL_HEIGHT = Config.getConfigInt(Config.KEY_KMZ_IMAGE_HEIGHT);
331                 if (THUMBNAIL_HEIGHT < DEFAULT_THUMBNAIL_HEIGHT) {THUMBNAIL_HEIGHT = DEFAULT_THUMBNAIL_HEIGHT;}
332                 // Create array for image dimensions in case it's required
333                 _imageDimensions = new Dimension[_track.getNumPoints()];
334
335                 OutputStreamWriter writer = null;
336                 ZipOutputStream zipOutputStream = null;
337                 try
338                 {
339                         // Select writer according to whether kmz requested or not
340                         if (!_kmzCheckbox.isSelected())
341                         {
342                                 // normal writing to file
343                                 writer = new OutputStreamWriter(new FileOutputStream(_exportFile));
344                         }
345                         else
346                         {
347                                 // kmz requested - need zip output stream
348                                 zipOutputStream = new ZipOutputStream(new FileOutputStream(_exportFile));
349                                 // Export images into zip file too if requested
350                                 if (exportImages)
351                                 {
352                                         // Create thumbnails of each photo in turn and add to zip as images/image<n>.jpg
353                                         // This is done first so that photo sizes are known for later
354                                         exportThumbnails(zipOutputStream);
355                                 }
356                                 writer = new OutputStreamWriter(zipOutputStream);
357                                 // Make an entry in the zip file for the kml file
358                                 ZipEntry kmlEntry = new ZipEntry(KML_FILENAME_IN_KMZ);
359                                 zipOutputStream.putNextEntry(kmlEntry);
360                         }
361                         // write file
362                         final int numPoints = exportData(writer, exportImages);
363                         // update config with selected track colour
364                         Config.setConfigString(Config.KEY_KML_TRACK_COLOUR, ColourUtils.makeHexCode(_colourPatch.getBackground()));
365                         // update progress bar
366                         _progressBar.setValue(1);
367
368                         // close zip entry if necessary
369                         if (zipOutputStream != null)
370                         {
371                                 // Make sure all buffered data in writer is flushed
372                                 writer.flush();
373                                 // Close off this entry in the zip file
374                                 zipOutputStream.closeEntry();
375                         }
376
377                         // close file
378                         writer.close();
379                         _imageDimensions = null;
380                         // Store directory in config for later
381                         Config.setConfigString(Config.KEY_TRACK_DIR, _exportFile.getParentFile().getAbsolutePath());
382                         // show confirmation
383                         UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.save.ok1")
384                                  + " " + numPoints + " " + I18nManager.getText("confirm.save.ok2")
385                                  + " " + _exportFile.getAbsolutePath());
386                         // export successful so need to close dialog and return
387                         _dialog.dispose();
388                         return;
389                 }
390                 catch (IOException ioe)
391                 {
392                         try {
393                                 if (writer != null) writer.close();
394                         }
395                         catch (IOException ioe2) {}
396                         JOptionPane.showMessageDialog(_parentFrame,
397                                 I18nManager.getText("error.save.failed") + " : " + ioe.getMessage(),
398                                 I18nManager.getText("error.save.dialogtitle"), JOptionPane.ERROR_MESSAGE);
399                 }
400                 // if not returned already, export failed so need to recall the file selection
401                 startExport();
402         }
403
404
405         /**
406          * Export the information to the given writer
407          * @param inWriter writer object
408          * @param inExportImages true if image thumbnails are to be referenced
409          * @return number of points written
410          */
411         private int exportData(OutputStreamWriter inWriter, boolean inExportImages)
412         throws IOException
413         {
414                 boolean writeTrack = _pointTypeSelector.getTrackpointsSelected();
415                 boolean writeWaypoints = _pointTypeSelector.getWaypointsSelected();
416                 boolean writePhotos = _pointTypeSelector.getPhotopointsSelected();
417                 boolean justSelection = _pointTypeSelector.getJustSelection();
418                 inWriter.write("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<kml xmlns=\"http://earth.google.com/kml/2.1\">\n<Folder>\n");
419                 inWriter.write("\t<name>");
420                 if (_descriptionField != null && _descriptionField.getText() != null && !_descriptionField.getText().equals(""))
421                 {
422                         inWriter.write(_descriptionField.getText());
423                 }
424                 else
425                 {
426                         inWriter.write("Export from Prune");
427                 }
428                 inWriter.write("</name>\n");
429
430                 // Examine selection if required
431                 int selStart = -1, selEnd = -1;
432                 if (justSelection) {
433                         selStart = _trackInfo.getSelection().getStart();
434                         selEnd = _trackInfo.getSelection().getEnd();
435                 }
436
437                 boolean absoluteAltitudes = _altitudesCheckbox.isSelected();
438                 int i = 0;
439                 DataPoint point = null;
440                 boolean hasTrackpoints = false;
441                 boolean writtenPhotoHeader = false;
442                 final int numPoints = _track.getNumPoints();
443                 int numSaved = 0;
444                 int photoNum = 0;
445                 // Loop over waypoints
446                 for (i=0; i<numPoints; i++)
447                 {
448                         point = _track.getPoint(i);
449                         boolean writeCurrentPoint = !justSelection || (i>=selStart && i<=selEnd);
450                         // Make a blob for each waypoint
451                         if (point.isWaypoint())
452                         {
453                                 if (writeWaypoints && writeCurrentPoint)
454                                 {
455                                         exportWaypoint(point, inWriter, absoluteAltitudes);
456                                         numSaved++;
457                                 }
458                         }
459                         else if (point.getPhoto() == null)
460                         {
461                                 hasTrackpoints = true;
462                         }
463                         // Make a blob with description for each photo
464                         // Photos have already been written so picture sizes already known
465                         if (point.getPhoto() != null && writePhotos && writeCurrentPoint)
466                         {
467                                 if (!writtenPhotoHeader)
468                                 {
469                                         inWriter.write("<Style id=\"camera_icon\"><IconStyle><Icon><href>http://maps.google.com/mapfiles/kml/pal4/icon46.png</href></Icon></IconStyle></Style>");
470                                         writtenPhotoHeader = true;
471                                 }
472                                 photoNum++;
473                                 exportPhotoPoint(point, inWriter, inExportImages, i, photoNum, absoluteAltitudes);
474                                 numSaved++;
475                         }
476                 }
477                 // Make a line for the track, if there is one
478                 if (hasTrackpoints && writeTrack)
479                 {
480                         // Set up strings for start and end of track segment
481                         String trackStart = "\t<Placemark>\n\t\t<name>track</name>\n\t\t<Style>\n\t\t\t<LineStyle>\n"
482                                 + "\t\t\t\t<color>cc" + reverse(ColourUtils.makeHexCode(_colourPatch.getBackground())) + "</color>\n"
483                                 + "\t\t\t\t<width>4</width>\n\t\t\t</LineStyle>\n"
484                                 + "\t\t\t<PolyStyle><color>33cc0000</color></PolyStyle>\n"
485                                 + "\t\t</Style>\n\t\t<LineString>\n";
486                         if (absoluteAltitudes) {
487                                 trackStart += "\t\t\t<extrude>1</extrude>\n\t\t\t<altitudeMode>absolute</altitudeMode>\n";
488                         }
489                         else {
490                                 trackStart += "\t\t\t<altitudeMode>clampToGround</altitudeMode>\n";
491                         }
492                         trackStart += "\t\t\t<coordinates>";
493                         String trackEnd = "\t\t\t</coordinates>\n\t\t</LineString>\n\t</Placemark>";
494
495                         // Start segment
496                         inWriter.write(trackStart);
497                         // Loop over track points
498                         boolean firstTrackpoint = true;
499                         for (i=0; i<numPoints; i++)
500                         {
501                                 point = _track.getPoint(i);
502                                 boolean writeCurrentPoint = !justSelection || (i>=selStart && i<=selEnd);
503                                 if (!point.isWaypoint() && writeCurrentPoint)
504                                 {
505                                         // start new track segment if necessary
506                                         if (point.getSegmentStart() && !firstTrackpoint) {
507                                                 inWriter.write(trackEnd);
508                                                 inWriter.write(trackStart);
509                                         }
510                                         if (point.getPhoto() == null)
511                                         {
512                                                 exportTrackpoint(point, inWriter);
513                                                 numSaved++;
514                                                 firstTrackpoint = false;
515                                         }
516                                 }
517                         }
518                         // end segment
519                         inWriter.write(trackEnd);
520                 }
521                 inWriter.write("</Folder>\n</kml>");
522                 return numSaved;
523         }
524
525         /**
526          * Reverse the hex code for the colours for KML's stupid backwards format
527          * @param inCode colour code rrggbb
528          * @return kml code bbggrr
529          */
530         private static String reverse(String inCode)
531         {
532                 return inCode.substring(4, 6) + inCode.substring(2, 4) + inCode.substring(0, 2);
533         }
534
535         /**
536          * Export the specified waypoint into the file
537          * @param inPoint waypoint to export
538          * @param inWriter writer object
539          * @param inAbsoluteAltitude true for absolute altitude
540          * @throws IOException on write failure
541          */
542         private void exportWaypoint(DataPoint inPoint, Writer inWriter, boolean inAbsoluteAltitude) throws IOException
543         {
544                 inWriter.write("\t<Placemark>\n\t\t<name>");
545                 inWriter.write(inPoint.getWaypointName().trim());
546                 inWriter.write("</name>\n");
547                 inWriter.write("\t\t<Point>\n");
548                 if (inAbsoluteAltitude && inPoint.hasAltitude()) {
549                         inWriter.write("\t\t\t<altitudeMode>absolute</altitudeMode>\n");
550                 }
551                 else {
552                         inWriter.write("\t\t\t<altitudeMode>clampToGround</altitudeMode>\n");
553                 }
554                 inWriter.write("\t\t\t<coordinates>");
555                 inWriter.write(inPoint.getLongitude().output(Coordinate.FORMAT_DECIMAL_FORCE_POINT));
556                 inWriter.write(',');
557                 inWriter.write(inPoint.getLatitude().output(Coordinate.FORMAT_DECIMAL_FORCE_POINT));
558                 inWriter.write(",");
559                 if (inPoint.hasAltitude()) {
560                         inWriter.write("" + inPoint.getAltitude().getStringValue(Altitude.Format.METRES));
561                 }
562                 else {
563                         inWriter.write("0");
564                 }
565                 inWriter.write("</coordinates>\n\t\t</Point>\n\t</Placemark>\n");
566         }
567
568
569         /**
570          * Export the specified photo into the file
571          * @param inPoint data point including photo
572          * @param inWriter writer object
573          * @param inImageLink flag to set whether to export image links or not
574          * @param inPointNumber number of point for accessing dimensions
575          * @param inImageNumber number of image for filename
576          * @param inAbsoluteAltitude true for absolute altitudes
577          * @throws IOException on write failure
578          */
579         private void exportPhotoPoint(DataPoint inPoint, Writer inWriter, boolean inImageLink,
580                 int inPointNumber, int inImageNumber, boolean inAbsoluteAltitude)
581         throws IOException
582         {
583                 inWriter.write("\t<Placemark>\n\t\t<name>");
584                 inWriter.write(inPoint.getPhoto().getFile().getName());
585                 inWriter.write("</name>\n");
586                 if (inImageLink)
587                 {
588                         Dimension imageSize = _imageDimensions[inPointNumber];
589                         // Write out some html for the thumbnail images
590                         inWriter.write("<description><![CDATA[<br/><table border='0'><tr><td><center><img src='images/image"
591                                 + inImageNumber + ".jpg' width='" + imageSize.width + "' height='" + imageSize.height + "'></center></td></tr>"
592                                 + "<tr><td><center>" + inPoint.getPhoto().getFile().getName() + "</center></td></tr></table>]]></description>");
593                 }
594                 inWriter.write("<styleUrl>#camera_icon</styleUrl>\n");
595                 inWriter.write("\t\t<Point>\n");
596                 if (inAbsoluteAltitude && inPoint.hasAltitude()) {
597                         inWriter.write("\t\t\t<altitudeMode>absolute</altitudeMode>\n");
598                 }
599                 else {
600                         inWriter.write("\t\t\t<altitudeMode>clampToGround</altitudeMode>\n");
601                 }
602                 inWriter.write("\t\t\t<coordinates>");
603                 inWriter.write(inPoint.getLongitude().output(Coordinate.FORMAT_DECIMAL_FORCE_POINT));
604                 inWriter.write(',');
605                 inWriter.write(inPoint.getLatitude().output(Coordinate.FORMAT_DECIMAL_FORCE_POINT));
606                 inWriter.write(",");
607                 // Altitude if point has one
608                 if (inPoint.hasAltitude()) {
609                         inWriter.write("" + inPoint.getAltitude().getStringValue(Altitude.Format.METRES));
610                 }
611                 else {
612                         inWriter.write("0");
613                 }
614                 inWriter.write("</coordinates>\n\t\t</Point>\n\t</Placemark>\n");
615         }
616
617
618         /**
619          * Export the specified trackpoint into the file
620          * @param inPoint trackpoint to export
621          * @param inWriter writer object
622          */
623         private void exportTrackpoint(DataPoint inPoint, Writer inWriter) throws IOException
624         {
625                 inWriter.write(inPoint.getLongitude().output(Coordinate.FORMAT_DECIMAL_FORCE_POINT));
626                 inWriter.write(',');
627                 inWriter.write(inPoint.getLatitude().output(Coordinate.FORMAT_DECIMAL_FORCE_POINT));
628                 // Altitude if point has one
629                 inWriter.write(",");
630                 if (inPoint.hasAltitude()) {
631                         inWriter.write("" + inPoint.getAltitude().getStringValue(Altitude.Format.METRES));
632                 }
633                 else {
634                         inWriter.write("0");
635                 }
636                 inWriter.write("\n");
637         }
638
639
640         /**
641          * Loop through the photos and create thumbnails
642          * @param inZipStream zip stream to save image files to
643          */
644         private void exportThumbnails(ZipOutputStream inZipStream) throws IOException
645         {
646                 // set up image writer
647                 Iterator<ImageWriter> writers = ImageIO.getImageWritersByFormatName("jpg");
648                 if (writers == null || !writers.hasNext())
649                 {
650                         throw new IOException("no JPEG writer found");
651                 }
652                 ImageWriter imageWriter = writers.next();
653
654                 // Check selection checkbox
655                 boolean justSelection = _pointTypeSelector.getJustSelection();
656                 int selStart = -1, selEnd = -1;
657                 if (justSelection) {
658                         selStart = _trackInfo.getSelection().getStart();
659                         selEnd = _trackInfo.getSelection().getEnd();
660                 }
661
662                 int numPoints = _track.getNumPoints();
663                 DataPoint point = null;
664                 int photoNum = 0;
665                 // Loop over all points in track
666                 for (int i=0; i<numPoints && !_cancelPressed; i++)
667                 {
668                         point = _track.getPoint(i);
669                         if (point.getPhoto() != null && (!justSelection || (i>=selStart && i<=selEnd)))
670                         {
671                                 photoNum++;
672                                 // Make a new entry in zip file
673                                 ZipEntry entry = new ZipEntry("images/image" + photoNum + ".jpg");
674                                 inZipStream.putNextEntry(entry);
675                                 // Load image and write to outstream
676                                 ImageIcon icon = new ImageIcon(point.getPhoto().getFile().getAbsolutePath());
677
678                                 // Scale and smooth image to required size
679                                 BufferedImage bufferedImage = ImageUtils.rotateImage(icon.getImage(),
680                                         THUMBNAIL_WIDTH, THUMBNAIL_HEIGHT, point.getPhoto().getRotationDegrees());
681                                 // Store image dimensions so that it doesn't have to be calculated again for the points
682                                 _imageDimensions[i] = new Dimension(bufferedImage.getWidth(), bufferedImage.getHeight());
683
684                                 imageWriter.setOutput(ImageIO.createImageOutputStream(inZipStream));
685                                 imageWriter.write(bufferedImage);
686                                 // Close zip file entry
687                                 inZipStream.closeEntry();
688                                 // Update progress bar
689                                 _progressBar.setValue(photoNum+1);
690                         }
691                 }
692         }
693
694
695         /**
696          * @return number of correlated photos in the track
697          */
698         private int getNumPhotosToExport()
699         {
700                 int numPoints = _track.getNumPoints();
701                 int numPhotos = 0;
702                 DataPoint point = null;
703                 // Loop over all points in track
704                 for (int i=0; i<numPoints; i++)
705                 {
706                         point = _track.getPoint(i);
707                         if (point.getPhoto() != null) {
708                                 numPhotos++;
709                         }
710                 }
711                 return numPhotos;
712         }
713 }