]> gitweb.fperrin.net Git - GpsPrune.git/blob - tim/prune/save/KmlExporter.java
Version 5, May 2008
[GpsPrune.git] / tim / prune / save / KmlExporter.java
1 package tim.prune.save;
2
3 import java.awt.BorderLayout;
4 import java.awt.Component;
5 import java.awt.Dimension;
6 import java.awt.FlowLayout;
7 import java.awt.event.ActionEvent;
8 import java.awt.event.ActionListener;
9 import java.awt.image.BufferedImage;
10 import java.io.File;
11 import java.io.FileOutputStream;
12 import java.io.IOException;
13 import java.io.OutputStreamWriter;
14 import java.io.Writer;
15 import java.util.Iterator;
16 import java.util.zip.ZipEntry;
17 import java.util.zip.ZipOutputStream;
18
19 import javax.imageio.ImageIO;
20 import javax.imageio.ImageWriter;
21 import javax.swing.Box;
22 import javax.swing.BoxLayout;
23 import javax.swing.ImageIcon;
24 import javax.swing.JButton;
25 import javax.swing.JCheckBox;
26 import javax.swing.JDialog;
27 import javax.swing.JFileChooser;
28 import javax.swing.JFrame;
29 import javax.swing.JLabel;
30 import javax.swing.JOptionPane;
31 import javax.swing.JPanel;
32 import javax.swing.JProgressBar;
33 import javax.swing.JTextField;
34 import javax.swing.SwingConstants;
35 import javax.swing.filechooser.FileFilter;
36
37 import tim.prune.I18nManager;
38 import tim.prune.UpdateMessageBroker;
39 import tim.prune.data.Altitude;
40 import tim.prune.data.Coordinate;
41 import tim.prune.data.DataPoint;
42 import tim.prune.data.Field;
43 import tim.prune.data.Track;
44 import tim.prune.data.TrackInfo;
45 import tim.prune.gui.ImageUtils;
46
47 /**
48  * Class to export track information
49  * into a specified Kml file
50  */
51 public class KmlExporter implements Runnable
52 {
53         private JFrame _parentFrame = null;
54         private TrackInfo _trackInfo = null;
55         private Track _track = null;
56         private JDialog _dialog = null;
57         private JTextField _descriptionField = null;
58         private JCheckBox _altitudesCheckbox = null;
59         private JCheckBox _kmzCheckbox = null;
60         private JCheckBox _exportImagesCheckbox = null;
61         private JProgressBar _progressBar = null;
62         private JFileChooser _fileChooser = null;
63         private File _exportFile = null;
64
65         // Filename of Kml file within zip archive
66         private static final String KML_FILENAME_IN_KMZ = "doc.kml";
67         // Width and height of thumbnail images in Kmz
68         private static final int THUMBNAIL_WIDTH = 240;
69         private static final int THUMBNAIL_HEIGHT = 180;
70
71
72         /**
73          * Constructor giving frame and track
74          * @param inParentFrame parent frame
75          * @param inTrackInfo track info object to save
76          */
77         public KmlExporter(JFrame inParentFrame, TrackInfo inTrackInfo)
78         {
79                 _parentFrame = inParentFrame;
80                 _trackInfo = inTrackInfo;
81                 _track = inTrackInfo.getTrack();
82         }
83
84
85         /**
86          * Show the dialog to select options and export file
87          */
88         public void showDialog()
89         {
90                 // Make dialog window including whether to compress to kmz (and include pictures) or not
91                 if (_dialog == null)
92                 {
93                         _dialog = new JDialog(_parentFrame, I18nManager.getText("dialog.exportkml.title"), true);
94                         _dialog.setLocationRelativeTo(_parentFrame);
95                         _dialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE);
96                         _dialog.getContentPane().add(makeDialogComponents());
97                         _dialog.pack();
98                 }
99                 enableCheckboxes();
100                 _progressBar.setVisible(false);
101                 _dialog.show();
102         }
103
104
105         /**
106          * Create dialog components
107          * @return Panel containing all gui elements in dialog
108          */
109         private Component makeDialogComponents()
110         {
111                 JPanel dialogPanel = new JPanel();
112                 dialogPanel.setLayout(new BorderLayout());
113                 JPanel mainPanel = new JPanel();
114                 mainPanel.setLayout(new BoxLayout(mainPanel, BoxLayout.Y_AXIS));
115                 // Make a central panel with the text box and checkboxes
116                 JPanel descPanel = new JPanel();
117                 descPanel.setLayout(new FlowLayout());
118                 descPanel.add(new JLabel(I18nManager.getText("dialog.exportkml.text")));
119                 _descriptionField = new JTextField(20);
120                 descPanel.add(_descriptionField);
121                 mainPanel.add(descPanel);
122                 dialogPanel.add(mainPanel, BorderLayout.CENTER);
123                 // Checkbox for altitude export
124                 _altitudesCheckbox = new JCheckBox(I18nManager.getText("dialog.exportkml.altitude"));
125                 _altitudesCheckbox.setHorizontalTextPosition(SwingConstants.LEFT);
126                 mainPanel.add(_altitudesCheckbox);
127                 // Checkboxes for kmz export and image export
128                 _kmzCheckbox = new JCheckBox(I18nManager.getText("dialog.exportkml.kmz"));
129                 _kmzCheckbox.setHorizontalTextPosition(SwingConstants.LEFT);
130                 _kmzCheckbox.addActionListener(new ActionListener() {
131                         public void actionPerformed(ActionEvent e)
132                         {
133                                 // enable image checkbox if kmz activated
134                                 enableCheckboxes();
135                         }
136                 });
137                 mainPanel.add(_kmzCheckbox);
138                 _exportImagesCheckbox = new JCheckBox(I18nManager.getText("dialog.exportkml.exportimages"));
139                 _exportImagesCheckbox.setHorizontalTextPosition(SwingConstants.LEFT);
140                 mainPanel.add(_exportImagesCheckbox);
141                 mainPanel.add(Box.createVerticalStrut(10));
142                 _progressBar = new JProgressBar(0, 100);
143                 _progressBar.setVisible(false);
144                 mainPanel.add(_progressBar);
145                 mainPanel.add(Box.createVerticalStrut(10));
146                 // button panel at bottom
147                 JPanel buttonPanel = new JPanel();
148                 buttonPanel.setLayout(new FlowLayout(FlowLayout.RIGHT));
149                 JButton okButton = new JButton(I18nManager.getText("button.ok"));
150                 ActionListener okListener = new ActionListener() {
151                         public void actionPerformed(ActionEvent e)
152                         {
153                                 startExport();
154                         }
155                 };
156                 okButton.addActionListener(okListener);
157                 _descriptionField.addActionListener(okListener);
158                 buttonPanel.add(okButton);
159                 JButton cancelButton = new JButton(I18nManager.getText("button.cancel"));
160                 cancelButton.addActionListener(new ActionListener() {
161                         public void actionPerformed(ActionEvent e)
162                         {
163                                 _dialog.dispose();
164                         }
165                 });
166                 buttonPanel.add(cancelButton);
167                 dialogPanel.add(buttonPanel, BorderLayout.SOUTH);
168                 return dialogPanel;
169         }
170
171
172         /**
173          * Enable the checkboxes according to data
174          */
175         private void enableCheckboxes()
176         {
177                 boolean hasAltitudes = _track.hasData(Field.ALTITUDE);
178                 if (!hasAltitudes) {_altitudesCheckbox.setSelected(false);}
179                 boolean hasPhotos = _trackInfo.getPhotoList() != null && _trackInfo.getPhotoList().getNumPhotos() > 0;
180                 _exportImagesCheckbox.setSelected(hasPhotos && _kmzCheckbox.isSelected());
181                 _exportImagesCheckbox.setEnabled(hasPhotos && _kmzCheckbox.isSelected());
182         }
183
184
185         /**
186          * Start the export process based on the input parameters
187          */
188         private void startExport()
189         {
190                 // OK pressed, so choose output file
191                 if (_fileChooser == null)
192                         {_fileChooser = new JFileChooser();}
193                 _fileChooser.setDialogType(JFileChooser.SAVE_DIALOG);
194                 _fileChooser.setFileFilter(new FileFilter() {
195                         public boolean accept(File f)
196                         {
197                                 return (f != null && (f.isDirectory()
198                                         || f.getName().toLowerCase().endsWith(".kml") || f.getName().toLowerCase().endsWith(".kmz")));
199                         }
200                         public String getDescription()
201                         {
202                                 return I18nManager.getText("dialog.exportkml.filetype");
203                         }
204                 });
205                 String requiredExtension = null, otherExtension = null;
206                 if (_kmzCheckbox.isSelected())
207                 {
208                         requiredExtension = ".kmz"; otherExtension = ".kml";
209                 }
210                 else
211                 {
212                         requiredExtension = ".kml"; otherExtension = ".kmz";
213                 }
214                 _fileChooser.setAcceptAllFileFilterUsed(false);
215                 // Allow choose again if an existing file is selected
216                 boolean chooseAgain = false;
217                 do
218                 {
219                         chooseAgain = false;
220                         if (_fileChooser.showSaveDialog(_parentFrame) == JFileChooser.APPROVE_OPTION)
221                         {
222                                 // OK pressed and file chosen
223                                 File file = _fileChooser.getSelectedFile();
224                                 if (file.getName().toLowerCase().endsWith(otherExtension))
225                                 {
226                                         String path = file.getAbsolutePath();
227                                         file = new File(path.substring(0, path.length()-otherExtension.length()) + requiredExtension);
228                                 }
229                                 else if (!file.getName().toLowerCase().endsWith(requiredExtension))
230                                 {
231                                         file = new File(file.getAbsolutePath() + requiredExtension);
232                                 }
233                                 // Check if file exists and if necessary prompt for overwrite
234                                 Object[] buttonTexts = {I18nManager.getText("button.overwrite"), I18nManager.getText("button.cancel")};
235                                 if (!file.exists() || JOptionPane.showOptionDialog(_parentFrame,
236                                                 I18nManager.getText("dialog.save.overwrite.text"),
237                                                 I18nManager.getText("dialog.save.overwrite.title"), JOptionPane.YES_NO_OPTION,
238                                                 JOptionPane.WARNING_MESSAGE, null, buttonTexts, buttonTexts[1])
239                                         == JOptionPane.YES_OPTION)
240                                 {
241                                         // New file or overwrite confirmed, so initiate export in separate thread
242                                         _exportFile = file;
243                                         new Thread(this).start();
244                                 }
245                                 else
246                                 {
247                                         chooseAgain = true;
248                                 }
249                         }
250                 } while (chooseAgain);
251         }
252
253
254         /**
255          * Run method for controlling separate thread for exporting
256          */
257         public void run()
258         {
259                 // Initialise progress bar
260                 _progressBar.setVisible(true);
261                 _progressBar.setValue(0);
262                 boolean exportToKmz = _kmzCheckbox.isSelected();
263                 boolean exportImages = exportToKmz && _exportImagesCheckbox.isSelected();
264                 _progressBar.setMaximum(exportImages?getNumPhotosToExport():1);
265                 OutputStreamWriter writer = null;
266                 ZipOutputStream zipOutputStream = null;
267                 try
268                 {
269                         // Select writer according to whether kmz requested or not
270                         if (!_kmzCheckbox.isSelected())
271                         {
272                                 // normal writing to file
273                                 writer = new OutputStreamWriter(new FileOutputStream(_exportFile));
274                         }
275                         else
276                         {
277                                 // kmz requested - need zip output stream
278                                 zipOutputStream = new ZipOutputStream(new FileOutputStream(_exportFile));
279                                 writer = new OutputStreamWriter(zipOutputStream);
280                                 // Make an entry in the zip file for the kml file
281                                 ZipEntry kmlEntry = new ZipEntry(KML_FILENAME_IN_KMZ);
282                                 zipOutputStream.putNextEntry(kmlEntry);
283                         }
284                         // write file
285                         int numPoints = exportData(writer, exportImages);
286                         // update progress bar
287                         _progressBar.setValue(1);
288
289                         // close zip entry if necessary
290                         if (zipOutputStream != null)
291                         {
292                                 // Make sure all buffered data in writer is flushed
293                                 writer.flush();
294                                 // Close off this entry in the zip file
295                                 zipOutputStream.closeEntry();
296                                 // Export images into zip file too if requested
297                                 if (exportImages)
298                                 {
299                                         // Create thumbnails of each photo in turn and add to zip as images/image<n>.jpg
300                                         exportThumbnails(zipOutputStream);
301                                 }
302                         }
303
304                         // close file
305                         writer.close();
306                         // show confirmation
307                         UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.save.ok1")
308                                  + " " + numPoints + " " + I18nManager.getText("confirm.save.ok2")
309                                  + " " + _exportFile.getAbsolutePath());
310                         // export successful so need to close dialog and return
311                         _dialog.dispose();
312                         return;
313                 }
314                 catch (IOException ioe)
315                 {
316                         // System.out.println("Exception: " + ioe.getClass().getName() + " - " + ioe.getMessage());
317                         try {
318                                 if (writer != null) writer.close();
319                         }
320                         catch (IOException ioe2) {}
321                         JOptionPane.showMessageDialog(_parentFrame,
322                                 I18nManager.getText("error.save.failed") + " : " + ioe.getMessage(),
323                                 I18nManager.getText("error.save.dialogtitle"), JOptionPane.ERROR_MESSAGE);
324                 }
325                 // if not returned already, export failed so need to recall the file selection
326                 startExport();
327         }
328
329
330         /**
331          * Export the information to the given writer
332          * @param inWriter writer object
333          * @param inExportImages true if image thumbnails are to be referenced
334          * @return number of points written
335          */
336         private int exportData(OutputStreamWriter inWriter, boolean inExportImages)
337         throws IOException
338         {
339                 inWriter.write("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<kml xmlns=\"http://earth.google.com/kml/2.1\">\n<Folder>\n");
340                 inWriter.write("\t<name>");
341                 if (_descriptionField != null && _descriptionField.getText() != null && !_descriptionField.getText().equals(""))
342                 {
343                         inWriter.write(_descriptionField.getText());
344                 }
345                 else
346                 {
347                         inWriter.write("Export from Prune");
348                 }
349                 inWriter.write("</name>\n");
350
351                 boolean exportAltitudes = _altitudesCheckbox.isSelected();
352                 int i = 0;
353                 DataPoint point = null;
354                 boolean hasTrackpoints = false;
355                 // Loop over waypoints
356                 boolean writtenPhotoHeader = false;
357                 int numPoints = _track.getNumPoints();
358                 int photoNum = 0;
359                 for (i=0; i<numPoints; i++)
360                 {
361                         point = _track.getPoint(i);
362                         // Make a blob for each waypoint
363                         if (point.isWaypoint())
364                         {
365                                 exportWaypoint(point, inWriter, exportAltitudes);
366                         }
367                         else if (point.getPhoto() == null)
368                         {
369                                 hasTrackpoints = true;
370                         }
371                         // Make a blob with description for each photo
372                         if (point.getPhoto() != null)
373                         {
374                                 if (!writtenPhotoHeader)
375                                 {
376                                         inWriter.write("<Style id=\"camera_icon\"><IconStyle><Icon><href>http://maps.google.com/mapfiles/kml/pal4/icon46.png</href></Icon></IconStyle></Style>");
377                                         writtenPhotoHeader = true;
378                                 }
379                                 photoNum++;
380                                 exportPhotoPoint(point, inWriter, inExportImages, photoNum, exportAltitudes);
381                         }
382                 }
383                 // Make a line for the track, if there is one
384                 if (hasTrackpoints)
385                 {
386                         // Set up strings for start and end of track segment
387                         String trackStart = "\t<Placemark>\n\t\t<name>track</name>\n\t\t<Style>\n\t\t\t<LineStyle>\n"
388                                 + "\t\t\t\t<color>cc0000cc</color>\n\t\t\t\t<width>4</width>\n\t\t\t</LineStyle>\n"
389                                 + "\t\t\t<PolyStyle><color>33cc0000</color></PolyStyle>\n"
390                                 + "\t\t</Style>\n\t\t<LineString>\n";
391                         if (exportAltitudes) {
392                                 trackStart += "\t\t\t<extrude>1</extrude>\n\t\t\t<altitudeMode>absolute</altitudeMode>\n";
393                         }
394                         trackStart += "\t\t\t<coordinates>";
395                         String trackEnd = "\t\t\t</coordinates>\n\t\t</LineString>\n\t</Placemark>";
396
397                         // Start segment
398                         inWriter.write(trackStart);
399                         // Loop over track points
400                         boolean firstTrackpoint = true;
401                         for (i=0; i<numPoints; i++)
402                         {
403                                 point = _track.getPoint(i);
404                                 // start new track segment if necessary
405                                 if (point.getSegmentStart() && !firstTrackpoint) {
406                                         inWriter.write(trackEnd);
407                                         inWriter.write(trackStart);
408                                 }
409                                 if (!point.isWaypoint() && point.getPhoto() == null)
410                                 {
411                                         exportTrackpoint(point, inWriter, exportAltitudes);
412                                         firstTrackpoint = false;
413                                 }
414                         }
415                         // end segment
416                         inWriter.write(trackEnd);
417                 }
418                 inWriter.write("</Folder>\n</kml>");
419                 return numPoints;
420         }
421
422
423         /**
424          * Export the specified waypoint into the file
425          * @param inPoint waypoint to export
426          * @param inWriter writer object
427          * @param inExportAltitude true to include altitude
428          * @throws IOException on write failure
429          */
430         private void exportWaypoint(DataPoint inPoint, Writer inWriter, boolean inExportAltitude) throws IOException
431         {
432                 inWriter.write("\t<Placemark>\n\t\t<name>");
433                 inWriter.write(inPoint.getWaypointName().trim());
434                 inWriter.write("</name>\n");
435                 inWriter.write("\t\t<Point>\n");
436                 if (inExportAltitude && inPoint.hasAltitude()) {
437                         inWriter.write("\t\t\t<altitudeMode>absolute</altitudeMode>\n");
438                 }
439                 inWriter.write("\t\t\t<coordinates>");
440                 inWriter.write(inPoint.getLongitude().output(Coordinate.FORMAT_DEG_WITHOUT_CARDINAL));
441                 inWriter.write(',');
442                 inWriter.write(inPoint.getLatitude().output(Coordinate.FORMAT_DEG_WITHOUT_CARDINAL));
443                 inWriter.write(",");
444                 if (inExportAltitude && inPoint.hasAltitude()) {
445                         inWriter.write("" + inPoint.getAltitude().getValue(Altitude.FORMAT_METRES));
446                 }
447                 else {
448                         inWriter.write("0");
449                 }
450                 inWriter.write("</coordinates>\n\t\t</Point>\n\t</Placemark>\n");
451         }
452
453
454         /**
455          * Export the specified photo into the file
456          * @param inPoint data point including photo
457          * @param inWriter writer object
458          * @param inImageLink flag to set whether to export image links or not
459          * @param inImageNumber number of image for filename
460          * @param inExportAltitude true to include altitude
461          * @throws IOException on write failure
462          */
463         private void exportPhotoPoint(DataPoint inPoint, Writer inWriter, boolean inImageLink,
464                 int inImageNumber, boolean inExportAltitude)
465         throws IOException
466         {
467                 inWriter.write("\t<Placemark>\n\t\t<name>");
468                 inWriter.write(inPoint.getPhoto().getFile().getName());
469                 inWriter.write("</name>\n");
470                 if (inImageLink)
471                 {
472                         // Work out image dimensions of thumbnail
473                         Dimension picSize = inPoint.getPhoto().getSize();
474                         Dimension thumbSize = ImageUtils.getThumbnailSize(picSize.width, picSize.height, THUMBNAIL_WIDTH, THUMBNAIL_HEIGHT);
475                         // Write out some html for the thumbnail images
476                         inWriter.write("<description><![CDATA[<br/><table border='0'><tr><td><center><img src='images/image"
477                                 + inImageNumber + ".jpg' width='" + thumbSize.width + "' height='" + thumbSize.height + "'></center></td></tr>"
478                                 + "<tr><td><center>Caption for the photo</center></td></tr></table>]]></description>");
479                 }
480                 inWriter.write("<styleUrl>#camera_icon</styleUrl>\n");
481                 inWriter.write("\t\t<Point>\n");
482                 if (inExportAltitude && inPoint.hasAltitude()) {
483                         inWriter.write("\t\t\t<altitudeMode>absolute</altitudeMode>\n");
484                 }
485                 inWriter.write("\t\t\t<coordinates>");
486                 inWriter.write(inPoint.getLongitude().output(Coordinate.FORMAT_DEG_WITHOUT_CARDINAL));
487                 inWriter.write(',');
488                 inWriter.write(inPoint.getLatitude().output(Coordinate.FORMAT_DEG_WITHOUT_CARDINAL));
489                 inWriter.write(",");
490                 if (inExportAltitude && inPoint.hasAltitude()) {
491                         inWriter.write("" + inPoint.getAltitude().getValue(Altitude.FORMAT_METRES));
492                 }
493                 else {
494                         inWriter.write("0");
495                 }
496                 inWriter.write("</coordinates>\n\t\t</Point>\n\t</Placemark>\n");
497         }
498
499
500         /**
501          * Export the specified trackpoint into the file
502          * @param inPoint trackpoint to export
503          * @param inWriter writer object
504          * @param inExportAltitude true to include altitude
505          */
506         private void exportTrackpoint(DataPoint inPoint, Writer inWriter, boolean inExportAltitude) throws IOException
507         {
508                 inWriter.write(inPoint.getLongitude().output(Coordinate.FORMAT_DEG_WITHOUT_CARDINAL));
509                 inWriter.write(',');
510                 inWriter.write(inPoint.getLatitude().output(Coordinate.FORMAT_DEG_WITHOUT_CARDINAL));
511                 // Altitude either absolute or locked to ground by Google Earth
512                 inWriter.write(",");
513                 if (inExportAltitude && inPoint.hasAltitude()) {
514                         inWriter.write("" + inPoint.getAltitude().getValue(Altitude.FORMAT_METRES));
515                 }
516                 else {
517                         inWriter.write("0");
518                 }
519                 inWriter.write("\n");
520         }
521
522
523         /**
524          * Loop through the photos and create thumbnails
525          * @param inZipStream zip stream to save image files to
526          */
527         private void exportThumbnails(ZipOutputStream inZipStream) throws IOException
528         {
529                 // set up image writer
530                 Iterator writers = ImageIO.getImageWritersByFormatName("jpg");
531                 if (writers == null || !writers.hasNext())
532                 {
533                         throw new IOException("no JPEG writer found");
534                 }
535                 ImageWriter imageWriter = (ImageWriter) writers.next();
536
537                 int numPoints = _track.getNumPoints();
538                 DataPoint point = null;
539                 int photoNum = 0;
540                 // Loop over all points in track
541                 for (int i=0; i<numPoints; i++)
542                 {
543                         point = _track.getPoint(i);
544                         if (point.getPhoto() != null)
545                         {
546                                 photoNum++;
547                                 // Make a new entry in zip file
548                                 ZipEntry entry = new ZipEntry("images/image" + photoNum + ".jpg");
549                                 inZipStream.putNextEntry(entry);
550                                 // Load image and write to outstream
551                                 ImageIcon icon = new ImageIcon(point.getPhoto().getFile().getAbsolutePath());
552
553                                 // Scale and smooth image to required size
554                                 Dimension outputSize = ImageUtils.getThumbnailSize(
555                                         point.getPhoto().getWidth(), point.getPhoto().getHeight(),
556                                         THUMBNAIL_WIDTH, THUMBNAIL_HEIGHT);
557                                 BufferedImage bufferedImage = ImageUtils.createScaledImage(icon.getImage(), outputSize.width, outputSize.height);
558
559                                 imageWriter.setOutput(ImageIO.createImageOutputStream(inZipStream));
560                                 imageWriter.write(bufferedImage);
561                                 // Close zip file entry
562                                 inZipStream.closeEntry();
563                                 // Update progress bar
564                                 _progressBar.setValue(photoNum+1);
565                         }
566                 }
567         }
568
569
570         /**
571          * @return number of correlated photos in the track
572          */
573         private int getNumPhotosToExport()
574         {
575                 int numPoints = _track.getNumPoints();
576                 int numPhotos = 0;
577                 DataPoint point = null;
578                 // Loop over all points in track
579                 for (int i=0; i<numPoints; i++)
580                 {
581                         point = _track.getPoint(i);
582                         if (point.getPhoto() != null)
583                         {
584                                 numPhotos++;
585                         }
586                 }
587                 return numPhotos;
588         }
589 }