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