]> gitweb.fperrin.net Git - GpsPrune.git/blob - src/tim/prune/save/KmlExporter.java
9d1c0789b4f4d9a3e6820f5883b7f7235bac74ab
[GpsPrune.git] / src / 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.ButtonGroup;
27 import javax.swing.ImageIcon;
28 import javax.swing.JButton;
29 import javax.swing.JCheckBox;
30 import javax.swing.JDialog;
31 import javax.swing.JFileChooser;
32 import javax.swing.JLabel;
33 import javax.swing.JOptionPane;
34 import javax.swing.JPanel;
35 import javax.swing.JProgressBar;
36 import javax.swing.JRadioButton;
37 import javax.swing.JTextField;
38 import javax.swing.SwingConstants;
39
40 import tim.prune.App;
41 import tim.prune.GenericFunction;
42 import tim.prune.I18nManager;
43 import tim.prune.UpdateMessageBroker;
44 import tim.prune.config.ColourUtils;
45 import tim.prune.config.Config;
46 import tim.prune.data.Coordinate;
47 import tim.prune.data.DataPoint;
48 import tim.prune.data.Field;
49 import tim.prune.data.RecentFile;
50 import tim.prune.data.Timestamp;
51 import tim.prune.data.Track;
52 import tim.prune.data.TrackInfo;
53 import tim.prune.data.UnitSetLibrary;
54 import tim.prune.gui.DialogCloser;
55 import tim.prune.gui.ImageUtils;
56 import tim.prune.gui.WholeNumberField;
57 import tim.prune.gui.colour.ColourChooser;
58 import tim.prune.gui.colour.ColourPatch;
59 import tim.prune.load.GenericFileFilter;
60 import tim.prune.save.xml.XmlUtils;
61
62 /**
63  * Class to export track information
64  * into a specified Kml or Kmz file
65  */
66 public class KmlExporter extends GenericFunction implements Runnable
67 {
68         private TrackInfo _trackInfo = null;
69         private Track _track = null;
70         private JDialog _dialog = null;
71         private JTextField _descriptionField = null;
72         private PointTypeSelector _pointTypeSelector = null;
73         private JRadioButton _gxExtensionsRadio = null;
74         private JCheckBox _altitudesCheckbox = null;
75         private JCheckBox _kmzCheckbox = null;
76         private JCheckBox _exportImagesCheckbox = null;
77         private JLabel _imageSizeLabel = null;
78         private WholeNumberField _imageSizeField = null;
79         private ColourPatch _colourPatch = null;
80         private JLabel _progressLabel = null;
81         private JProgressBar _progressBar = null;
82         private Dimension[] _imageDimensions = null;
83         private JFileChooser _fileChooser = null;
84         private File _exportFile = null;
85         private JButton _okButton = null;
86         private boolean _cancelPressed = false;
87         private ColourChooser _colourChooser = null;
88
89         // Filename of Kml file within zip archive
90         private static final String KML_FILENAME_IN_KMZ = "doc.kml";
91         // Default width and height of thumbnail images in Kmz
92         private static final int DEFAULT_THUMBNAIL_WIDTH = 240;
93         // Default track colour
94         private static final Color DEFAULT_TRACK_COLOUR = new Color(204, 0, 0); // red
95
96
97         /**
98          * Constructor
99          * @param inApp app object
100          */
101         public KmlExporter(App inApp)
102         {
103                 super(inApp);
104                 _trackInfo = inApp.getTrackInfo();
105                 _track = _trackInfo.getTrack();
106         }
107
108         /** Get name key */
109         public String getNameKey() {
110                 return "function.exportkml";
111         }
112
113         /**
114          * Show the dialog to select options and export file
115          */
116         public void begin()
117         {
118                 // Make dialog window including whether to compress to kmz (and include pictures) or not
119                 if (_dialog == null)
120                 {
121                         _dialog = new JDialog(_parentFrame, I18nManager.getText(getNameKey()), true);
122                         _dialog.setLocationRelativeTo(_parentFrame);
123                         _dialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE);
124                         _dialog.getContentPane().add(makeDialogComponents());
125                         _dialog.pack();
126                         _colourChooser = new ColourChooser(_dialog);
127                 }
128                 // Fill in image size from config
129                 _imageSizeField.setValue(Config.getConfigInt(Config.KEY_KMZ_IMAGE_SIZE));
130                 enableCheckboxes();
131                 _descriptionField.setEnabled(true);
132                 _okButton.setEnabled(true);
133                 _progressLabel.setText("");
134                 _progressBar.setVisible(false);
135                 _dialog.setVisible(true);
136         }
137
138
139         /**
140          * Create dialog components
141          * @return Panel containing all gui elements in dialog
142          */
143         private Component makeDialogComponents()
144         {
145                 JPanel dialogPanel = new JPanel();
146                 dialogPanel.setLayout(new BorderLayout(0, 5));
147                 JPanel mainPanel = new JPanel();
148                 mainPanel.setLayout(new BoxLayout(mainPanel, BoxLayout.Y_AXIS));
149                 // Make a central panel with the text box and checkboxes
150                 JPanel descPanel = new JPanel();
151                 descPanel.setLayout(new FlowLayout());
152                 descPanel.add(new JLabel(I18nManager.getText("dialog.exportkml.text")));
153                 _descriptionField = new JTextField(20);
154                 _descriptionField.addKeyListener(new DialogCloser(_dialog));
155                 descPanel.add(_descriptionField);
156                 descPanel.setAlignmentX(Component.CENTER_ALIGNMENT);
157                 mainPanel.add(descPanel);
158                 dialogPanel.add(mainPanel, BorderLayout.CENTER);
159                 // point type selection
160                 _pointTypeSelector = new PointTypeSelector();
161                 _pointTypeSelector.setAlignmentX(Component.CENTER_ALIGNMENT);
162                 mainPanel.add(_pointTypeSelector);
163                 // Colour definition
164                 Color trackColour = ColourUtils.colourFromHex(Config.getConfigString(Config.KEY_KML_TRACK_COLOUR));
165                 if (trackColour == null) {
166                         trackColour = DEFAULT_TRACK_COLOUR;
167                 }
168                 _colourPatch = new ColourPatch(trackColour);
169                 _colourPatch.addMouseListener(new MouseAdapter() {
170                         public void mouseClicked(MouseEvent e) {
171                                 _colourChooser.showDialog(_colourPatch.getBackground());
172                                 Color colour = _colourChooser.getChosenColour();
173                                 if (colour != null) _colourPatch.setColour(colour);
174                         }
175                 });
176                 JPanel colourPanel = new JPanel();
177                 colourPanel.add(new JLabel(I18nManager.getText("dialog.exportkml.trackcolour")));
178                 colourPanel.add(_colourPatch);
179                 mainPanel.add(colourPanel);
180                 // Pair of radio buttons for standard/extended KML
181                 JRadioButton standardKmlRadio = new JRadioButton(I18nManager.getText("dialog.exportkml.standardkml"));
182                 _gxExtensionsRadio = new JRadioButton(I18nManager.getText("dialog.exportkml.extendedkml"));
183                 ButtonGroup bGroup = new ButtonGroup();
184                 bGroup.add(standardKmlRadio); bGroup.add(_gxExtensionsRadio);
185                 JPanel radioPanel = new JPanel();
186                 radioPanel.setLayout(new FlowLayout(FlowLayout.CENTER, 10, 1));
187                 radioPanel.add(standardKmlRadio);
188                 radioPanel.add(_gxExtensionsRadio);
189                 standardKmlRadio.setSelected(true);
190                 mainPanel.add(radioPanel);
191                 // Checkbox for altitude export
192                 _altitudesCheckbox = new JCheckBox(I18nManager.getText("dialog.exportkml.altitude"));
193                 _altitudesCheckbox.setHorizontalTextPosition(SwingConstants.LEFT);
194                 _altitudesCheckbox.setAlignmentX(Component.CENTER_ALIGNMENT);
195                 mainPanel.add(_altitudesCheckbox);
196
197                 // Checkboxes for kmz export and image export
198                 _kmzCheckbox = new JCheckBox(I18nManager.getText("dialog.exportkml.kmz"));
199                 _kmzCheckbox.setHorizontalTextPosition(SwingConstants.LEFT);
200                 _kmzCheckbox.setAlignmentX(Component.CENTER_ALIGNMENT);
201                 // enable image checkbox if kmz activated
202                 _kmzCheckbox.addActionListener(new ActionListener() {
203                         public void actionPerformed(ActionEvent e)
204                         {
205                                 enableCheckboxes();
206                         }
207                 });
208                 mainPanel.add(_kmzCheckbox);
209                 _exportImagesCheckbox = new JCheckBox(I18nManager.getText("dialog.exportkml.exportimages"));
210                 _exportImagesCheckbox.setHorizontalTextPosition(SwingConstants.LEFT);
211                 _exportImagesCheckbox.setAlignmentX(Component.CENTER_ALIGNMENT);
212                 // enable image size fields if image checkbox changes
213                 _exportImagesCheckbox.addActionListener(new ActionListener() {
214                         public void actionPerformed(ActionEvent arg0) {
215                                 enableImageSizeFields();
216                         }
217                 });
218                 mainPanel.add(_exportImagesCheckbox);
219                 // Panel for the image size
220                 JPanel imageSizePanel = new JPanel();
221                 imageSizePanel.setLayout(new FlowLayout(FlowLayout.CENTER));
222                 _imageSizeLabel = new JLabel(I18nManager.getText("dialog.exportkml.imagesize"));
223                 _imageSizeLabel.setAlignmentX(Component.RIGHT_ALIGNMENT);
224                 imageSizePanel.add(_imageSizeLabel);
225                 _imageSizeField = new WholeNumberField(4);
226                 imageSizePanel.add(_imageSizeField);
227                 mainPanel.add(imageSizePanel);
228
229                 mainPanel.add(Box.createVerticalStrut(10));
230                 _progressLabel = new JLabel("...");
231                 _progressLabel.setAlignmentX(Component.CENTER_ALIGNMENT);
232                 mainPanel.add(_progressLabel);
233                 _progressBar = new JProgressBar(0, 100);
234                 _progressBar.setVisible(false);
235                 _progressBar.setAlignmentX(Component.CENTER_ALIGNMENT);
236                 mainPanel.add(_progressBar);
237                 mainPanel.add(Box.createVerticalStrut(10));
238                 // button panel at bottom
239                 JPanel buttonPanel = new JPanel();
240                 buttonPanel.setLayout(new FlowLayout(FlowLayout.RIGHT));
241                 _okButton = new JButton(I18nManager.getText("button.ok"));
242                 ActionListener okListener = new ActionListener() {
243                         public void actionPerformed(ActionEvent e)
244                         {
245                                 startExport();
246                         }
247                 };
248                 _okButton.addActionListener(okListener);
249                 _descriptionField.addActionListener(okListener);
250                 buttonPanel.add(_okButton);
251                 JButton cancelButton = new JButton(I18nManager.getText("button.cancel"));
252                 cancelButton.addActionListener(new ActionListener() {
253                         public void actionPerformed(ActionEvent e)
254                         {
255                                 _cancelPressed = true;
256                                 _dialog.dispose();
257                         }
258                 });
259                 buttonPanel.add(cancelButton);
260                 dialogPanel.add(buttonPanel, BorderLayout.SOUTH);
261                 return dialogPanel;
262         }
263
264
265         /**
266          * Enable the checkboxes according to data
267          */
268         private void enableCheckboxes()
269         {
270                 _pointTypeSelector.init(_trackInfo);
271                 boolean hasAltitudes = _track.hasData(Field.ALTITUDE);
272                 if (!hasAltitudes) {_altitudesCheckbox.setSelected(false);}
273                 boolean hasPhotos = _trackInfo.getPhotoList() != null && _trackInfo.getPhotoList().getNumPhotos() > 0;
274                 _exportImagesCheckbox.setSelected(hasPhotos && _kmzCheckbox.isSelected());
275                 _exportImagesCheckbox.setEnabled(hasPhotos && _kmzCheckbox.isSelected());
276                 enableImageSizeFields();
277         }
278
279         /**
280          * Enable and disable the image size fields according to the checkboxes
281          */
282         private void enableImageSizeFields()
283         {
284                 boolean exportImages = _exportImagesCheckbox.isEnabled() && _exportImagesCheckbox.isSelected();
285                 _imageSizeField.setEnabled(exportImages);
286                 _imageSizeLabel.setEnabled(exportImages);
287         }
288
289
290         /**
291          * @return true if using gx extensions for kml export
292          */
293         private boolean useGxExtensions() {
294                 return _gxExtensionsRadio.isSelected();
295         }
296         /**
297          * Start the export process based on the input parameters
298          */
299         private void startExport()
300         {
301                 // OK pressed, now validate selection checkboxes
302                 if (!_pointTypeSelector.getAnythingSelected()) {
303                         JOptionPane.showMessageDialog(_parentFrame, I18nManager.getText("dialog.save.notypesselected"),
304                                 I18nManager.getText("dialog.saveoptions.title"), JOptionPane.WARNING_MESSAGE);
305                         return;
306                 }
307                 // Choose output file
308                 if (_fileChooser == null)
309                 {
310                         _fileChooser = new JFileChooser();
311                         _fileChooser.setDialogType(JFileChooser.SAVE_DIALOG);
312                         _fileChooser.setFileFilter(new GenericFileFilter("filetype.kmlkmz", new String[] {"kml", "kmz"}));
313                         // start from directory in config which should be set
314                         String configDir = Config.getConfigString(Config.KEY_TRACK_DIR);
315                         if (configDir != null) {_fileChooser.setCurrentDirectory(new File(configDir));}
316                 }
317                 String requiredExtension = null, otherExtension = null;
318                 if (_kmzCheckbox.isSelected()) {
319                         requiredExtension = ".kmz"; otherExtension = ".kml";
320                 }
321                 else {
322                         requiredExtension = ".kml"; otherExtension = ".kmz";
323                 }
324                 _fileChooser.setAcceptAllFileFilterUsed(false);
325                 // Allow choose again if an existing file is selected
326                 boolean chooseAgain = false;
327                 do
328                 {
329                         chooseAgain = false;
330                         if (_fileChooser.showSaveDialog(_parentFrame) == JFileChooser.APPROVE_OPTION)
331                         {
332                                 // OK pressed and file chosen
333                                 File file = _fileChooser.getSelectedFile();
334                                 if (file.getName().toLowerCase().endsWith(otherExtension))
335                                 {
336                                         String path = file.getAbsolutePath();
337                                         file = new File(path.substring(0, path.length()-otherExtension.length()) + requiredExtension);
338                                 }
339                                 else if (!file.getName().toLowerCase().endsWith(requiredExtension))
340                                 {
341                                         file = new File(file.getAbsolutePath() + requiredExtension);
342                                 }
343                                 // Check if file exists and if necessary prompt for overwrite
344                                 Object[] buttonTexts = {I18nManager.getText("button.overwrite"), I18nManager.getText("button.cancel")};
345                                 if (!file.exists() || JOptionPane.showOptionDialog(_parentFrame,
346                                                 I18nManager.getText("dialog.save.overwrite.text"),
347                                                 I18nManager.getText("dialog.save.overwrite.title"), JOptionPane.YES_NO_OPTION,
348                                                 JOptionPane.WARNING_MESSAGE, null, buttonTexts, buttonTexts[1])
349                                         == JOptionPane.YES_OPTION)
350                                 {
351                                         // New file or overwrite confirmed, so initiate export in separate thread
352                                         _exportFile = file;
353                                         _cancelPressed = false;
354                                         new Thread(this).start();
355                                 }
356                                 else
357                                 {
358                                         chooseAgain = true;
359                                 }
360                         }
361                 } while (chooseAgain);
362         }
363
364
365         /**
366          * Run method for controlling separate thread for exporting
367          */
368         public void run()
369         {
370                 // Disable ok button to stop second go
371                 _okButton.setEnabled(false);
372                 _descriptionField.setEnabled(false);
373                 // Initialise progress indicators
374                 _progressLabel.setText(I18nManager.getText("confirm.running"));
375                 _progressBar.setVisible(true);
376                 _progressBar.setValue(0);
377                 boolean exportToKmz = _kmzCheckbox.isSelected();
378                 boolean exportImages = exportToKmz && _exportImagesCheckbox.isSelected();
379                 _progressBar.setMaximum(exportImages?getNumPhotosToExport():1);
380
381                 // Create array for image dimensions in case it's required
382                 _imageDimensions = new Dimension[_track.getNumPoints()];
383
384                 OutputStreamWriter writer = null;
385                 ZipOutputStream zipOutputStream = null;
386                 try
387                 {
388                         // Select writer according to whether kmz requested or not
389                         if (!_kmzCheckbox.isSelected())
390                         {
391                                 // normal writing to file
392                                 writer = new OutputStreamWriter(new FileOutputStream(_exportFile));
393                         }
394                         else
395                         {
396                                 // kmz requested - need zip output stream
397                                 zipOutputStream = new ZipOutputStream(new FileOutputStream(_exportFile));
398                                 // Export images into zip file too if requested
399                                 if (exportImages)
400                                 {
401                                         // Get entered value for image size, store in config
402                                         int thumbSize = _imageSizeField.getValue();
403                                         if (thumbSize < DEFAULT_THUMBNAIL_WIDTH) {thumbSize = DEFAULT_THUMBNAIL_WIDTH;}
404                                         Config.setConfigInt(Config.KEY_KMZ_IMAGE_SIZE, thumbSize);
405
406                                         // Create thumbnails of each photo in turn and add to zip as images/image<n>.jpg
407                                         // This is done first so that photo sizes are known for later
408                                         exportThumbnails(zipOutputStream, thumbSize);
409                                 }
410                                 writer = new OutputStreamWriter(zipOutputStream);
411                                 // Make an entry in the zip file for the kml file
412                                 ZipEntry kmlEntry = new ZipEntry(KML_FILENAME_IN_KMZ);
413                                 zipOutputStream.putNextEntry(kmlEntry);
414                         }
415                         // write file
416                         final int numPoints = exportData(writer, exportImages);
417                         // update config with selected track colour
418                         Config.setConfigString(Config.KEY_KML_TRACK_COLOUR, ColourUtils.makeHexCode(_colourPatch.getBackground()));
419                         // update progress bar
420                         _progressBar.setValue(1);
421
422                         // close zip entry if necessary
423                         if (zipOutputStream != null)
424                         {
425                                 // Make sure all buffered data in writer is flushed
426                                 writer.flush();
427                                 // Close off this entry in the zip file
428                                 zipOutputStream.closeEntry();
429                         }
430
431                         // close file
432                         writer.close();
433                         _imageDimensions = null;
434                         // Store directory in config for later
435                         Config.setConfigString(Config.KEY_TRACK_DIR, _exportFile.getParentFile().getAbsolutePath());
436                         // Add to recent file list
437                         Config.getRecentFileList().addFile(new RecentFile(_exportFile, true));
438                         // show confirmation
439                         UpdateMessageBroker.informSubscribers();
440                         UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.save.ok1")
441                                  + " " + numPoints + " " + I18nManager.getText("confirm.save.ok2")
442                                  + " " + _exportFile.getAbsolutePath());
443                         // export successful so need to close dialog and return
444                         _dialog.dispose();
445                         return;
446                 }
447                 catch (IOException ioe)
448                 {
449                         try {
450                                 if (writer != null) writer.close();
451                         }
452                         catch (IOException ioe2) {}
453                         JOptionPane.showMessageDialog(_parentFrame,
454                                 I18nManager.getText("error.save.failed") + " : " + ioe.getMessage(),
455                                 I18nManager.getText("error.save.dialogtitle"), JOptionPane.ERROR_MESSAGE);
456                 }
457                 // if not returned already, export failed so need to recall the file selection
458                 startExport();
459         }
460
461
462         /**
463          * Export the information to the given writer
464          * @param inWriter writer object
465          * @param inExportImages true if image thumbnails are to be referenced
466          * @return number of points written
467          */
468         private int exportData(OutputStreamWriter inWriter, boolean inExportImages)
469         throws IOException
470         {
471                 boolean writeTrack = _pointTypeSelector.getTrackpointsSelected();
472                 boolean writeWaypoints = _pointTypeSelector.getWaypointsSelected();
473                 boolean writePhotos = _pointTypeSelector.getPhotopointsSelected();
474                 boolean writeAudios = _pointTypeSelector.getAudiopointsSelected();
475                 boolean justSelection = _pointTypeSelector.getJustSelection();
476                 // Define xml header (depending on whether extensions are used or not)
477                 if (useGxExtensions()) {
478                         inWriter.write("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<kml xmlns=\"http://earth.google.com/kml/2.2\" xmlns:gx=\"http://www.google.com/kml/ext/2.2\">\n");
479                 }
480                 else {
481                         inWriter.write("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<kml xmlns=\"http://earth.google.com/kml/2.1\">\n");
482                 }
483                 inWriter.write("<Folder>\n\t<name>");
484                 if (_descriptionField != null && _descriptionField.getText() != null && !_descriptionField.getText().equals(""))
485                 {
486                         inWriter.write(XmlUtils.fixCdata(_descriptionField.getText()));
487                 }
488                 else {
489                         inWriter.write("Export from GpsPrune");
490                 }
491                 inWriter.write("</name>\n");
492
493                 // Examine selection if required
494                 int selStart = -1, selEnd = -1;
495                 if (justSelection) {
496                         selStart = _trackInfo.getSelection().getStart();
497                         selEnd = _trackInfo.getSelection().getEnd();
498                 }
499
500                 boolean absoluteAltitudes = _altitudesCheckbox.isSelected();
501                 int i = 0;
502                 DataPoint point = null;
503                 boolean hasTrackpoints = false;
504                 boolean writtenPhotoHeader = false, writtenAudioHeader = false;
505                 final int numPoints = _track.getNumPoints();
506                 int numSaved = 0;
507                 int photoNum = 0;
508                 // Loop over waypoints
509                 for (i=0; i<numPoints; i++)
510                 {
511                         point = _track.getPoint(i);
512                         boolean writeCurrentPoint = !justSelection || (i>=selStart && i<=selEnd);
513                         // Make a blob for each waypoint
514                         if (point.isWaypoint())
515                         {
516                                 if (writeWaypoints && writeCurrentPoint)
517                                 {
518                                         exportWaypoint(point, inWriter, absoluteAltitudes);
519                                         numSaved++;
520                                 }
521                         }
522                         else if (!point.hasMedia())
523                         {
524                                 hasTrackpoints = true;
525                         }
526                         // Make a blob with description for each photo
527                         // Photos have already been written so picture sizes already known
528                         if (point.getPhoto() != null && point.getPhoto().isValid() && writePhotos && writeCurrentPoint)
529                         {
530                                 if (!writtenPhotoHeader)
531                                 {
532                                         inWriter.write("<Style id=\"camera_icon\"><IconStyle><Icon><href>http://maps.google.com/mapfiles/kml/pal4/icon46.png</href></Icon></IconStyle></Style>");
533                                         writtenPhotoHeader = true;
534                                 }
535                                 photoNum++;
536                                 exportPhotoPoint(point, inWriter, inExportImages, i, photoNum, absoluteAltitudes);
537                                 numSaved++;
538                         }
539                         // Make a blob with description for each audio clip
540                         if (point.getAudio() != null && writeAudios && writeCurrentPoint)
541                         {
542                                 if (!writtenAudioHeader)
543                                 {
544                                         inWriter.write("<Style id=\"audio_icon\"><IconStyle><color>ff00ffff</color><Icon><href>http://maps.google.com/mapfiles/kml/shapes/star.png</href></Icon></IconStyle></Style>");
545                                         writtenAudioHeader = true;
546                                 }
547                                 exportAudioPoint(point, inWriter, absoluteAltitudes);
548                                 numSaved++;
549                         }
550                 }
551                 // Make a line for the track, if there is one
552                 if (hasTrackpoints && writeTrack)
553                 {
554                         boolean useGxExtensions = _gxExtensionsRadio.isSelected();
555                         if (useGxExtensions)
556                         {
557                                 // Write track using the Google Extensions to KML including gx:Track
558                                 numSaved += writeGxTrack(inWriter, absoluteAltitudes, selStart, selEnd);
559                         }
560                         else {
561                                 // Write track using standard KML
562                                 numSaved += writeStandardTrack(inWriter, absoluteAltitudes, selStart, selEnd);
563                         }
564                 }
565                 inWriter.write("</Folder>\n</kml>\n");
566                 return numSaved;
567         }
568
569
570         /**
571          * Write out the track using standard KML LineString tag
572          * @param inWriter writer object to write to
573          * @param inAbsoluteAltitudes true to use absolute altitudes, false to clamp to ground
574          * @param inSelStart start index of selection, or -1 if whole track
575          * @param inSelEnd     end index of selection, or -1 if whole track
576          * @return number of track points written
577          */
578         private int writeStandardTrack(OutputStreamWriter inWriter, boolean inAbsoluteAltitudes, int inSelStart,
579                 int inSelEnd)
580         throws IOException
581         {
582                 int numSaved = 0;
583                 // Set up strings for start and end of track segment
584                 String trackStart = "\t<Placemark>\n\t\t<name>track</name>\n\t\t<Style>\n\t\t\t<LineStyle>\n"
585                         + "\t\t\t\t<color>cc" + reverse(ColourUtils.makeHexCode(_colourPatch.getBackground())) + "</color>\n"
586                         + "\t\t\t\t<width>4</width>\n\t\t\t</LineStyle>\n"
587                         + "\t\t</Style>\n\t\t<LineString>\n";
588                 if (inAbsoluteAltitudes) {
589                         trackStart += "\t\t\t<extrude>1</extrude>\n\t\t\t<altitudeMode>absolute</altitudeMode>\n";
590                 }
591                 else {
592                         trackStart += "\t\t\t<altitudeMode>clampToGround</altitudeMode>\n";
593                 }
594                 trackStart += "\t\t\t<coordinates>";
595                 String trackEnd = "\t\t\t</coordinates>\n\t\t</LineString>\n\t</Placemark>";
596
597                 boolean justSelection = _pointTypeSelector.getJustSelection();
598
599                 // Start segment
600                 inWriter.write(trackStart);
601                 // Loop over track points
602                 boolean firstTrackpoint = true;
603                 final int numPoints = _track.getNumPoints();
604                 for (int i=0; i<numPoints; i++)
605                 {
606                         DataPoint point = _track.getPoint(i);
607                         boolean writeCurrentPoint = !justSelection || (i>=inSelStart && i<=inSelEnd);
608                         if (!point.isWaypoint() && writeCurrentPoint)
609                         {
610                                 // start new track segment if necessary
611                                 if (point.getSegmentStart() && !firstTrackpoint) {
612                                         inWriter.write(trackEnd);
613                                         inWriter.write(trackStart);
614                                 }
615                                 if (point.getPhoto() == null)
616                                 {
617                                         exportTrackpoint(point, inWriter);
618                                         numSaved++;
619                                         firstTrackpoint = false;
620                                 }
621                         }
622                 }
623                 // end segment
624                 inWriter.write(trackEnd);
625                 return numSaved;
626         }
627
628
629         /**
630          * Write out the track using Google's KML Extensions such as gx:Track
631          * @param inWriter writer object to write to
632          * @param inAbsoluteAltitudes true to use absolute altitudes, false to clamp to ground
633          * @param inSelStart start index of selection, or -1 if whole track
634          * @param inSelEnd     end index of selection, or -1 if whole track
635          * @return number of track points written
636          */
637         private int writeGxTrack(OutputStreamWriter inWriter, boolean inAbsoluteAltitudes, int inSelStart,
638                 int inSelEnd)
639         throws IOException
640         {
641                 int numSaved = 0;
642                 // Set up strings for start and end of track segment
643                 String trackStart = "\t<Placemark>\n\t\t<name>track</name>\n\t\t<Style>\n\t\t\t<LineStyle>\n"
644                         + "\t\t\t\t<color>cc" + reverse(ColourUtils.makeHexCode(_colourPatch.getBackground())) + "</color>\n"
645                         + "\t\t\t\t<width>4</width>\n\t\t\t</LineStyle>\n"
646                         + "\t\t</Style>\n\t\t<gx:Track>\n";
647                 if (inAbsoluteAltitudes) {
648                         trackStart += "\t\t\t<extrude>1</extrude>\n\t\t\t<altitudeMode>absolute</altitudeMode>\n";
649                 }
650                 else {
651                         trackStart += "\t\t\t<altitudeMode>clampToGround</altitudeMode>\n";
652                 }
653                 String trackEnd = "\n\t\t</gx:Track>\n\t</Placemark>\n";
654
655                 boolean justSelection = _pointTypeSelector.getJustSelection();
656
657                 // Start segment
658                 inWriter.write(trackStart);
659                 StringBuilder whenList = new StringBuilder();
660                 StringBuilder coordList = new StringBuilder();
661
662                 // Loop over track points
663                 boolean firstTrackpoint = true;
664                 final int numPoints = _track.getNumPoints();
665                 for (int i=0; i<numPoints; i++)
666                 {
667                         DataPoint point = _track.getPoint(i);
668                         boolean writeCurrentPoint = !justSelection || (i>=inSelStart && i<=inSelEnd);
669                         if (!point.isWaypoint() && writeCurrentPoint)
670                         {
671                                 // start new track segment if necessary
672                                 if (point.getSegmentStart() && !firstTrackpoint)
673                                 {
674                                         inWriter.write(whenList.toString());
675                                         inWriter.write('\n');
676                                         inWriter.write(coordList.toString());
677                                         inWriter.write('\n');
678                                         inWriter.write(trackEnd);
679                                         whenList.setLength(0); coordList.setLength(0);
680                                         inWriter.write(trackStart);
681                                 }
682                                 if (point.getPhoto() == null)
683                                 {
684                                         // Add timestamp (if any) to the list
685                                         whenList.append("<when>");
686                                         if (point.hasTimestamp()) {
687                                                 whenList.append(point.getTimestamp().getText(Timestamp.Format.ISO8601, null));
688                                         }
689                                         whenList.append("</when>\n");
690                                         // Add coordinates to the list
691                                         coordList.append("<gx:coord>");
692                                         coordList.append(point.getLongitude().output(Coordinate.FORMAT_DECIMAL_FORCE_POINT)).append(' ');
693                                         coordList.append(point.getLatitude().output(Coordinate.FORMAT_DECIMAL_FORCE_POINT)).append(' ');
694                                         if (point.hasAltitude()) {
695                                                 coordList.append("" + point.getAltitude().getStringValue(UnitSetLibrary.UNITS_METRES));
696                                         }
697                                         else {
698                                                 coordList.append('0');
699                                         }
700                                         coordList.append("</gx:coord>\n");
701                                         numSaved++;
702                                         firstTrackpoint = false;
703                                 }
704                         }
705                 }
706                 // end segment
707                 inWriter.write(whenList.toString());
708                 inWriter.write('\n');
709                 inWriter.write(coordList.toString());
710                 inWriter.write('\n');
711                 inWriter.write(trackEnd);
712                 return numSaved;
713         }
714
715
716         /**
717          * Reverse the hex code for the colours for KML's stupid backwards format
718          * @param inCode colour code rrggbb
719          * @return kml code bbggrr
720          */
721         private static String reverse(String inCode)
722         {
723                 return inCode.substring(4, 6) + inCode.substring(2, 4) + inCode.substring(0, 2);
724         }
725
726         /**
727          * Export the specified waypoint into the file
728          * @param inPoint waypoint to export
729          * @param inWriter writer object
730          * @param inAbsoluteAltitude true for absolute altitude
731          * @throws IOException on write failure
732          */
733         private void exportWaypoint(DataPoint inPoint, Writer inWriter, boolean inAbsoluteAltitude) throws IOException
734         {
735                 String name = inPoint.getWaypointName().trim();
736                 exportNamedPoint(inPoint, inWriter, name, inPoint.getFieldValue(Field.DESCRIPTION), null, inAbsoluteAltitude);
737         }
738
739
740         /**
741          * Export the specified audio point into the file
742          * @param inPoint audio point to export
743          * @param inWriter writer object
744          * @param inAbsoluteAltitude true for absolute altitude
745          * @throws IOException on write failure
746          */
747         private void exportAudioPoint(DataPoint inPoint, Writer inWriter, boolean inAbsoluteAltitude) throws IOException
748         {
749                 String name = inPoint.getAudio().getName();
750                 String desc = null;
751                 if (inPoint.getAudio().getFile() != null) {
752                         desc = inPoint.getAudio().getFile().getAbsolutePath();
753                 }
754                 exportNamedPoint(inPoint, inWriter, name, desc, "audio_icon", inAbsoluteAltitude);
755         }
756
757
758         /**
759          * Export the specified photo into the file
760          * @param inPoint data point including photo
761          * @param inWriter writer object
762          * @param inImageLink flag to set whether to export image links or not
763          * @param inPointNumber number of point for accessing dimensions
764          * @param inImageNumber number of image for filename
765          * @param inAbsoluteAltitude true for absolute altitudes
766          * @throws IOException on write failure
767          */
768         private void exportPhotoPoint(DataPoint inPoint, Writer inWriter, boolean inImageLink,
769                 int inPointNumber, int inImageNumber, boolean inAbsoluteAltitude)
770         throws IOException
771         {
772                 String name = inPoint.getPhoto().getName();
773                 String desc = null;
774                 if (inImageLink)
775                 {
776                         Dimension imageSize = _imageDimensions[inPointNumber];
777                         // Create html for the thumbnail images
778                         desc = "<![CDATA[<br/><table border='0'><tr><td><center><img src='images/image"
779                                 + inImageNumber + ".jpg' width='" + imageSize.width + "' height='" + imageSize.height + "'></center></td></tr>"
780                                 + "<tr><td><center>" + name + "</center></td></tr></table>]]>";
781                 }
782                 // Export point
783                 exportNamedPoint(inPoint, inWriter, name, desc, "camera_icon", inAbsoluteAltitude);
784         }
785
786
787         /**
788          * Export the specified named point into the file, like waypoint or photo point
789          * @param inPoint data point
790          * @param inWriter writer object
791          * @param inName name of point
792          * @param inDesc description of point, or null
793          * @param inStyle style of point, or null
794          * @param inAbsoluteAltitude true for absolute altitudes
795          * @throws IOException on write failure
796          */
797         private void exportNamedPoint(DataPoint inPoint, Writer inWriter, String inName,
798                 String inDesc, String inStyle, boolean inAbsoluteAltitude)
799         throws IOException
800         {
801                 inWriter.write("\t<Placemark>\n\t\t<name>");
802                 inWriter.write(XmlUtils.fixCdata(inName));
803                 inWriter.write("</name>\n");
804                 if (inDesc != null)
805                 {
806                         // Write out description
807                         inWriter.write("\t\t<description>");
808                         inWriter.write(XmlUtils.fixCdata(inDesc));
809                         inWriter.write("</description>\n");
810                 }
811                 if (inStyle != null)
812                 {
813                         inWriter.write("<styleUrl>#");
814                         inWriter.write(inStyle);
815                         inWriter.write("</styleUrl>\n");
816                 }
817                 inWriter.write("\t\t<Point>\n");
818                 if (inAbsoluteAltitude && inPoint.hasAltitude()) {
819                         inWriter.write("\t\t\t<altitudeMode>absolute</altitudeMode>\n");
820                 }
821                 else {
822                         inWriter.write("\t\t\t<altitudeMode>clampToGround</altitudeMode>\n");
823                 }
824                 inWriter.write("\t\t\t<coordinates>");
825                 inWriter.write(inPoint.getLongitude().output(Coordinate.FORMAT_DECIMAL_FORCE_POINT));
826                 inWriter.write(',');
827                 inWriter.write(inPoint.getLatitude().output(Coordinate.FORMAT_DECIMAL_FORCE_POINT));
828                 inWriter.write(',');
829                 // Altitude if point has one
830                 if (inPoint.hasAltitude()) {
831                         inWriter.write("" + inPoint.getAltitude().getStringValue(UnitSetLibrary.UNITS_METRES));
832                 }
833                 else {
834                         inWriter.write('0');
835                 }
836                 inWriter.write("</coordinates>\n\t\t</Point>\n\t</Placemark>\n");
837         }
838
839
840         /**
841          * Export the specified trackpoint into the file
842          * @param inPoint trackpoint to export
843          * @param inWriter writer object
844          */
845         private void exportTrackpoint(DataPoint inPoint, Writer inWriter) throws IOException
846         {
847                 inWriter.write(inPoint.getLongitude().output(Coordinate.FORMAT_DECIMAL_FORCE_POINT));
848                 inWriter.write(',');
849                 inWriter.write(inPoint.getLatitude().output(Coordinate.FORMAT_DECIMAL_FORCE_POINT));
850                 // Altitude if point has one
851                 inWriter.write(',');
852                 if (inPoint.hasAltitude()) {
853                         inWriter.write("" + inPoint.getAltitude().getStringValue(UnitSetLibrary.UNITS_METRES));
854                 }
855                 else {
856                         inWriter.write('0');
857                 }
858                 inWriter.write('\n');
859         }
860
861
862         /**
863          * Loop through the photos and create thumbnails
864          * @param inZipStream zip stream to save image files to
865          * @param inThumbSize thumbnail size
866          */
867         private void exportThumbnails(ZipOutputStream inZipStream, int inThumbSize)
868         throws IOException
869         {
870                 // set up image writer
871                 Iterator<ImageWriter> writers = ImageIO.getImageWritersByFormatName("jpg");
872                 if (writers == null || !writers.hasNext())
873                 {
874                         throw new IOException("no JPEG writer found");
875                 }
876                 ImageWriter imageWriter = writers.next();
877
878                 // Check selection checkbox
879                 final boolean justSelection = _pointTypeSelector.getJustSelection();
880                 int selStart = -1, selEnd = -1;
881                 if (justSelection) {
882                         selStart = _trackInfo.getSelection().getStart();
883                         selEnd = _trackInfo.getSelection().getEnd();
884                 }
885
886                 final int numPoints = _track.getNumPoints();
887                 DataPoint point = null;
888                 int photoNum = 0;
889                 // Loop over all points in track
890                 for (int i=0; i<numPoints && !_cancelPressed; i++)
891                 {
892                         point = _track.getPoint(i);
893                         if (point.getPhoto() != null && point.getPhoto().isValid() && (!justSelection || (i>=selStart && i<=selEnd)))
894                         {
895                                 photoNum++;
896                                 // Make a new entry in zip file
897                                 ZipEntry entry = new ZipEntry("images/image" + photoNum + ".jpg");
898                                 inZipStream.putNextEntry(entry);
899                                 // Load image and write to outstream
900                                 ImageIcon icon = point.getPhoto().createImageIcon();
901
902                                 // Scale image to required size (not smoothed)
903                                 BufferedImage bufferedImage = ImageUtils.rotateImage(icon.getImage(),
904                                         inThumbSize, inThumbSize, point.getPhoto().getRotationDegrees());
905                                 // Store image dimensions so that it doesn't have to be calculated again for the points
906                                 _imageDimensions[i] = new Dimension(bufferedImage.getWidth(), bufferedImage.getHeight());
907
908                                 imageWriter.setOutput(ImageIO.createImageOutputStream(inZipStream));
909                                 imageWriter.write(bufferedImage);
910                                 // Close zip file entry
911                                 inZipStream.closeEntry();
912                                 // Update progress bar
913                                 _progressBar.setValue(photoNum+1);
914                         }
915                 }
916         }
917
918
919         /**
920          * @return number of correlated photos in the track
921          */
922         private int getNumPhotosToExport()
923         {
924                 int numPoints = _track.getNumPoints();
925                 int numPhotos = 0;
926                 DataPoint point = null;
927                 // Loop over all points in track
928                 for (int i=0; i<numPoints; i++)
929                 {
930                         point = _track.getPoint(i);
931                         if (point.getPhoto() != null) {
932                                 numPhotos++;
933                         }
934                 }
935                 return numPhotos;
936         }
937 }