]> gitweb.fperrin.net Git - GpsPrune.git/blob - tim/prune/save/KmlExporter.java
Version 13, August 2011
[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.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;
57
58 /**
59  * Class to export track information
60  * into a specified Kml or Kmz file
61  */
62 public class KmlExporter extends GenericFunction implements Runnable
63 {
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;
81
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
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                 _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);
155                 // Colour definition
156                 Color trackColour = ColourUtils.colourFromHex(Config.getConfigString(Config.KEY_KML_TRACK_COLOUR));
157                 if (trackColour == null) {
158                         trackColour = DEFAULT_TRACK_COLOUR;
159                 }
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);
166                         }
167                 });
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)
183                         {
184                                 // enable image checkbox if kmz activated
185                                 enableCheckboxes();
186                         }
187                 });
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)
208                         {
209                                 startExport();
210                         }
211                 };
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)
218                         {
219                                 _cancelPressed = true;
220                                 _dialog.dispose();
221                         }
222                 });
223                 buttonPanel.add(cancelButton);
224                 dialogPanel.add(buttonPanel, BorderLayout.SOUTH);
225                 return dialogPanel;
226         }
227
228
229         /**
230          * Enable the checkboxes according to data
231          */
232         private void enableCheckboxes()
233         {
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());
240         }
241
242
243         /**
244          * Start the export process based on the input parameters
245          */
246         private void startExport()
247         {
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);
252                         return;
253                 }
254                 // Choose output file
255                 if (_fileChooser == null)
256                 {
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));}
263                 }
264                 String requiredExtension = null, otherExtension = null;
265                 if (_kmzCheckbox.isSelected()) {
266                         requiredExtension = ".kmz"; otherExtension = ".kml";
267                 }
268                 else {
269                         requiredExtension = ".kml"; otherExtension = ".kmz";
270                 }
271                 _fileChooser.setAcceptAllFileFilterUsed(false);
272                 // Allow choose again if an existing file is selected
273                 boolean chooseAgain = false;
274                 do
275                 {
276                         chooseAgain = false;
277                         if (_fileChooser.showSaveDialog(_parentFrame) == JFileChooser.APPROVE_OPTION)
278                         {
279                                 // OK pressed and file chosen
280                                 File file = _fileChooser.getSelectedFile();
281                                 if (file.getName().toLowerCase().endsWith(otherExtension))
282                                 {
283                                         String path = file.getAbsolutePath();
284                                         file = new File(path.substring(0, path.length()-otherExtension.length()) + requiredExtension);
285                                 }
286                                 else if (!file.getName().toLowerCase().endsWith(requiredExtension))
287                                 {
288                                         file = new File(file.getAbsolutePath() + requiredExtension);
289                                 }
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)
297                                 {
298                                         // New file or overwrite confirmed, so initiate export in separate thread
299                                         _exportFile = file;
300                                         _cancelPressed = false;
301                                         new Thread(this).start();
302                                 }
303                                 else
304                                 {
305                                         chooseAgain = true;
306                                 }
307                         }
308                 } while (chooseAgain);
309         }
310
311
312         /**
313          * Run method for controlling separate thread for exporting
314          */
315         public void run()
316         {
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);
327
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()];
335
336                 OutputStreamWriter writer = null;
337                 ZipOutputStream zipOutputStream = null;
338                 try
339                 {
340                         // Select writer according to whether kmz requested or not
341                         if (!_kmzCheckbox.isSelected())
342                         {
343                                 // normal writing to file
344                                 writer = new OutputStreamWriter(new FileOutputStream(_exportFile));
345                         }
346                         else
347                         {
348                                 // kmz requested - need zip output stream
349                                 zipOutputStream = new ZipOutputStream(new FileOutputStream(_exportFile));
350                                 // Export images into zip file too if requested
351                                 if (exportImages)
352                                 {
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);
356                                 }
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);
361                         }
362                         // write file
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);
368
369                         // close zip entry if necessary
370                         if (zipOutputStream != null)
371                         {
372                                 // Make sure all buffered data in writer is flushed
373                                 writer.flush();
374                                 // Close off this entry in the zip file
375                                 zipOutputStream.closeEntry();
376                         }
377
378                         // close file
379                         writer.close();
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));
385                         // show confirmation
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
391                         _dialog.dispose();
392                         return;
393                 }
394                 catch (IOException ioe)
395                 {
396                         try {
397                                 if (writer != null) writer.close();
398                         }
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);
403                 }
404                 // if not returned already, export failed so need to recall the file selection
405                 startExport();
406         }
407
408
409         /**
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
414          */
415         private int exportData(OutputStreamWriter inWriter, boolean inExportImages)
416         throws IOException
417         {
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(""))
426                 {
427                         inWriter.write(_descriptionField.getText());
428                 }
429                 else {
430                         inWriter.write("Export from GpsPrune");
431                 }
432                 inWriter.write("</name>\n");
433
434                 // Examine selection if required
435                 int selStart = -1, selEnd = -1;
436                 if (justSelection) {
437                         selStart = _trackInfo.getSelection().getStart();
438                         selEnd = _trackInfo.getSelection().getEnd();
439                 }
440
441                 boolean absoluteAltitudes = _altitudesCheckbox.isSelected();
442                 int i = 0;
443                 DataPoint point = null;
444                 boolean hasTrackpoints = false;
445                 boolean writtenPhotoHeader = false, writtenAudioHeader = false;
446                 final int numPoints = _track.getNumPoints();
447                 int numSaved = 0;
448                 int photoNum = 0;
449                 // Loop over waypoints
450                 for (i=0; i<numPoints; i++)
451                 {
452                         point = _track.getPoint(i);
453                         boolean writeCurrentPoint = !justSelection || (i>=selStart && i<=selEnd);
454                         // Make a blob for each waypoint
455                         if (point.isWaypoint())
456                         {
457                                 if (writeWaypoints && writeCurrentPoint)
458                                 {
459                                         exportWaypoint(point, inWriter, absoluteAltitudes);
460                                         numSaved++;
461                                 }
462                         }
463                         else if (!point.hasMedia())
464                         {
465                                 hasTrackpoints = true;
466                         }
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)
470                         {
471                                 if (!writtenPhotoHeader)
472                                 {
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;
475                                 }
476                                 photoNum++;
477                                 exportPhotoPoint(point, inWriter, inExportImages, i, photoNum, absoluteAltitudes);
478                                 numSaved++;
479                         }
480                         // Make a blob with description for each audio clip
481                         if (point.getAudio() != null && writeAudios && writeCurrentPoint)
482                         {
483                                 if (!writtenAudioHeader)
484                                 {
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;
487                                 }
488                                 exportAudioPoint(point, inWriter, absoluteAltitudes);
489                                 numSaved++;
490                         }
491                 }
492                 // Make a line for the track, if there is one
493                 if (hasTrackpoints && writeTrack)
494                 {
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";
503                         }
504                         else {
505                                 trackStart += "\t\t\t<altitudeMode>clampToGround</altitudeMode>\n";
506                         }
507                         trackStart += "\t\t\t<coordinates>";
508                         String trackEnd = "\t\t\t</coordinates>\n\t\t</LineString>\n\t</Placemark>";
509
510                         // Start segment
511                         inWriter.write(trackStart);
512                         // Loop over track points
513                         boolean firstTrackpoint = true;
514                         for (i=0; i<numPoints; i++)
515                         {
516                                 point = _track.getPoint(i);
517                                 boolean writeCurrentPoint = !justSelection || (i>=selStart && i<=selEnd);
518                                 if (!point.isWaypoint() && writeCurrentPoint)
519                                 {
520                                         // start new track segment if necessary
521                                         if (point.getSegmentStart() && !firstTrackpoint) {
522                                                 inWriter.write(trackEnd);
523                                                 inWriter.write(trackStart);
524                                         }
525                                         if (point.getPhoto() == null)
526                                         {
527                                                 exportTrackpoint(point, inWriter);
528                                                 numSaved++;
529                                                 firstTrackpoint = false;
530                                         }
531                                 }
532                         }
533                         // end segment
534                         inWriter.write(trackEnd);
535                 }
536                 inWriter.write("</Folder>\n</kml>");
537                 return numSaved;
538         }
539
540         /**
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
544          */
545         private static String reverse(String inCode)
546         {
547                 return inCode.substring(4, 6) + inCode.substring(2, 4) + inCode.substring(0, 2);
548         }
549
550         /**
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
556          */
557         private void exportWaypoint(DataPoint inPoint, Writer inWriter, boolean inAbsoluteAltitude) throws IOException
558         {
559                 String name = inPoint.getWaypointName().trim();
560                 exportNamedPoint(inPoint, inWriter, name, inPoint.getFieldValue(Field.DESCRIPTION), null, inAbsoluteAltitude);
561         }
562
563
564         /**
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
570          */
571         private void exportAudioPoint(DataPoint inPoint, Writer inWriter, boolean inAbsoluteAltitude) throws IOException
572         {
573                 String name = inPoint.getAudio().getName();
574                 String desc = null;
575                 if (inPoint.getAudio().getFile() != null) {
576                         desc = inPoint.getAudio().getFile().getAbsolutePath();
577                 }
578                 exportNamedPoint(inPoint, inWriter, name, desc, "audio_icon", inAbsoluteAltitude);
579         }
580
581
582         /**
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
591          */
592         private void exportPhotoPoint(DataPoint inPoint, Writer inWriter, boolean inImageLink,
593                 int inPointNumber, int inImageNumber, boolean inAbsoluteAltitude)
594         throws IOException
595         {
596                 String name = inPoint.getPhoto().getName();
597                 String desc = null;
598                 if (inImageLink)
599                 {
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>]]>";
605                 }
606                 // Export point
607                 exportNamedPoint(inPoint, inWriter, name, desc, "camera_icon", inAbsoluteAltitude);
608         }
609
610
611         /**
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
620          */
621         private void exportNamedPoint(DataPoint inPoint, Writer inWriter, String inName,
622                 String inDesc, String inStyle, boolean inAbsoluteAltitude)
623         throws IOException
624         {
625                 inWriter.write("\t<Placemark>\n\t\t<name>");
626                 inWriter.write(inName);
627                 inWriter.write("</name>\n");
628                 if (inDesc != null)
629                 {
630                         // Write out description
631                         inWriter.write("\t\t<description>");
632                         inWriter.write(XmlUtils.fixCdata(inDesc));
633                         inWriter.write("</description>\n");
634                 }
635                 if (inStyle != null)
636                 {
637                         inWriter.write("<styleUrl>#");
638                         inWriter.write(inStyle);
639                         inWriter.write("</styleUrl>\n");
640                 }
641                 inWriter.write("\t\t<Point>\n");
642                 if (inAbsoluteAltitude && inPoint.hasAltitude()) {
643                         inWriter.write("\t\t\t<altitudeMode>absolute</altitudeMode>\n");
644                 }
645                 else {
646                         inWriter.write("\t\t\t<altitudeMode>clampToGround</altitudeMode>\n");
647                 }
648                 inWriter.write("\t\t\t<coordinates>");
649                 inWriter.write(inPoint.getLongitude().output(Coordinate.FORMAT_DECIMAL_FORCE_POINT));
650                 inWriter.write(',');
651                 inWriter.write(inPoint.getLatitude().output(Coordinate.FORMAT_DECIMAL_FORCE_POINT));
652                 inWriter.write(',');
653                 // Altitude if point has one
654                 if (inPoint.hasAltitude()) {
655                         inWriter.write("" + inPoint.getAltitude().getStringValue(Altitude.Format.METRES));
656                 }
657                 else {
658                         inWriter.write('0');
659                 }
660                 inWriter.write("</coordinates>\n\t\t</Point>\n\t</Placemark>\n");
661         }
662
663
664         /**
665          * Export the specified trackpoint into the file
666          * @param inPoint trackpoint to export
667          * @param inWriter writer object
668          */
669         private void exportTrackpoint(DataPoint inPoint, Writer inWriter) throws IOException
670         {
671                 inWriter.write(inPoint.getLongitude().output(Coordinate.FORMAT_DECIMAL_FORCE_POINT));
672                 inWriter.write(',');
673                 inWriter.write(inPoint.getLatitude().output(Coordinate.FORMAT_DECIMAL_FORCE_POINT));
674                 // Altitude if point has one
675                 inWriter.write(',');
676                 if (inPoint.hasAltitude()) {
677                         inWriter.write("" + inPoint.getAltitude().getStringValue(Altitude.Format.METRES));
678                 }
679                 else {
680                         inWriter.write('0');
681                 }
682                 inWriter.write('\n');
683         }
684
685
686         /**
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
691          */
692         private void exportThumbnails(ZipOutputStream inZipStream, int inThumbWidth, int inThumbHeight)
693         throws IOException
694         {
695                 // set up image writer
696                 Iterator<ImageWriter> writers = ImageIO.getImageWritersByFormatName("jpg");
697                 if (writers == null || !writers.hasNext())
698                 {
699                         throw new IOException("no JPEG writer found");
700                 }
701                 ImageWriter imageWriter = writers.next();
702
703                 // Check selection checkbox
704                 boolean justSelection = _pointTypeSelector.getJustSelection();
705                 int selStart = -1, selEnd = -1;
706                 if (justSelection) {
707                         selStart = _trackInfo.getSelection().getStart();
708                         selEnd = _trackInfo.getSelection().getEnd();
709                 }
710
711                 int numPoints = _track.getNumPoints();
712                 DataPoint point = null;
713                 int photoNum = 0;
714                 // Loop over all points in track
715                 for (int i=0; i<numPoints && !_cancelPressed; i++)
716                 {
717                         point = _track.getPoint(i);
718                         if (point.getPhoto() != null && (!justSelection || (i>=selStart && i<=selEnd)))
719                         {
720                                 photoNum++;
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();
726
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());
732
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);
739                         }
740                 }
741         }
742
743
744         /**
745          * @return number of correlated photos in the track
746          */
747         private int getNumPhotosToExport()
748         {
749                 int numPoints = _track.getNumPoints();
750                 int numPhotos = 0;
751                 DataPoint point = null;
752                 // Loop over all points in track
753                 for (int i=0; i<numPoints; i++)
754                 {
755                         point = _track.getPoint(i);
756                         if (point.getPhoto() != null) {
757                                 numPhotos++;
758                         }
759                 }
760                 return numPhotos;
761         }
762 }