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