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